Skip to content

Commit bc1e72d

Browse files
committed
feat: optimize
1 parent 2c31af1 commit bc1e72d

10 files changed

Lines changed: 351 additions & 308 deletions

CMakeLists.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ if(WIN32)
5050
winhttp
5151
shell32
5252
psapi
53-
wbemuuid
5453
ole32
5554
oleaut32
5655
windowscodecs
@@ -63,7 +62,6 @@ if(WIN32)
6362
winhttp
6463
shell32
6564
psapi
66-
wbemuuid
6765
ole32
6866
oleaut32
6967
windowscodecs

include/file_watcher.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,12 @@ class FileWatcher {
5555
mutable std::mutex projectsMutex; // 스레드 안전성을 위한 뮤텍스
5656
std::deque<FileChangeEvent> pendingEvents;
5757
mutable std::mutex pendingEventsMutex;
58-
58+
std::atomic<bool> notifyScheduled{false}; // PostMessage 코얼레싱 (큐 적재 통지 1회로 합침)
59+
5960
// 파일 변경 이벤트 콜백 함수
6061
std::function<void(const FileChangeEvent&)> changeCallback;
62+
// 큐에 이벤트가 적재되었음을 메인 스레드에 통지 (PostMessage 등)
63+
std::function<void()> notifyCallback;
6164

6265
/**
6366
* 특정 프로젝트 폴더를 감시하는 워커 스레드 함수
@@ -96,6 +99,13 @@ class FileWatcher {
9699
* @param callback 파일이 변경될 때 호출될 함수
97100
*/
98101
void SetChangeCallback(std::function<void(const FileChangeEvent&)> callback);
102+
103+
/**
104+
* 큐 적재 통지 콜백 설정 (워커 스레드 → 메인 스레드 마샬링용).
105+
* 워커 스레드가 이벤트를 큐에 넣으면 이 콜백을 호출한다(코얼레싱됨).
106+
* @param callback 통지 시 호출될 함수 (예: PostMessage)
107+
*/
108+
void SetNotifyCallback(std::function<void()> callback);
99109

100110
/**
101111
* Unity 프로젝트 감시 시작

include/process_monitor.h

Lines changed: 20 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,38 @@
11
#pragma once
22

33
#include "globals.h"
4-
#include <Wbemidl.h> // WMI 인터페이스
54
#include <unordered_map>
65

7-
#pragma comment(lib, "wbemuuid.lib")
8-
#pragma comment(lib, "ole32.lib")
9-
#pragma comment(lib, "oleaut32.lib")
10-
116
/**
127
* Unity 프로세스들 감지
138
*/
149
class ProcessMonitor {
1510
private:
1611
std::unordered_map<DWORD, UnityInstance> activeInstances;
17-
IWbemLocator* pLocator = nullptr;
18-
IWbemServices* pService = nullptr;
19-
bool wmiInitialized = false;
20-
21-
/**
22-
* 문자열을 BSTR로 변환하는 헬퍼 함수
23-
* @param str 변환할 문자열
24-
* @return BSTR 포인터 (사용 후 SysFreeString으로 해제 필요)
25-
*/
26-
BSTR StringToBSTR(const std::wstring& str);
27-
28-
/**
29-
* BSTR을 문자열로 변환하는 헬퍼 함수
30-
* @param bstr 변환할 BSTR
31-
* @return 변환된 문자열
32-
*/
33-
std::string BSTRToString(BSTR bstr);
3412

3513
/**
36-
* WMI를 사용해서 실제 프로세스 커맨드 라인 가져오기
14+
* NtQueryInformationProcess + PEB 읽기로 프로세스 커맨드 라인을 가져온다.
15+
* WMI/RPC를 거치지 않아 Wmiprvse를 깨우지 않고 비용이 낮다.
16+
* (PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_READ 권한 필요, 64-bit 전제)
3717
* @param pid 대상 프로세스 ID
38-
* @return 커맨드 라인
39-
*/
40-
std::string GetRealCommandLine(DWORD pid);
41-
42-
/**
43-
* WMI 초기화 (COM 초기화)
18+
* @return 커맨드 라인, 실패시 빈 문자열
4419
*/
45-
bool InitializeWMI();
20+
std::string GetCommandLineViaPeb(DWORD pid);
4621

4722
/**
48-
* WMI 정리
23+
* 특정 프로세스의 커맨드 라인을 가져와 Unity 프로젝트 경로로 해석
24+
* @param pid 대상 프로세스 ID
25+
* @return 프로젝트 경로, 실패시 빈문자열
4926
*/
50-
void CleanupWMI();
27+
std::string GetProcessCommandLine(DWORD pid);
5128

5229
/**
53-
* 특정 프로세스의 커맨드 라인을 가져오기
30+
* 스냅샷 엔트리가 Unity 프로세스이면 인스턴스 정보로 해석
5431
* @param pid 대상 프로세스 ID
55-
* @return 커맨드 라인, 실패시 빈문자열
32+
* @param instance 해석된 인스턴스 (출력)
33+
* @return Unity 프로젝트로 해석되면 true
5634
*/
57-
std::string GetProcessCommandLine(DWORD pid);
35+
bool ResolveUnityInstance(DWORD pid, UnityInstance& instance);
5836

5937
/**
6038
* 커맨드 라인에서 Unity 프로젝트 경로 추출
@@ -96,33 +74,24 @@ class ProcessMonitor {
9674
~ProcessMonitor();
9775

9876
/**
99-
* 현재 실행 중인 모든 Unity 인스턴스를 스캔
77+
* 현재 실행 중인 모든 Unity 인스턴스를 스캔 (초기 스캔용).
78+
* 결과를 activeInstances에도 등록하여 이후 PollChanges가 중복 보고하지 않도록 한다.
10079
* @return 발견된 Unity 인스턴스들
10180
*/
10281
std::vector<UnityInstance> ScanUnityProcesses();
10382

10483
/**
105-
* 새로 시작된 Unity 프로세스가 있는지 확인
106-
* @return 새로운 인스턴스들
107-
*/
108-
std::vector<UnityInstance> GetNewInstances();
109-
110-
/**
111-
* 종료된 Unity 프로세스가 있는지 확인
112-
* @return 종료된 인스턴스들
84+
* 단일 스냅샷으로 새로 시작/종료된 Unity 프로세스를 한 번에 diff한다.
85+
* 이미 알려진 PID는 재해석(PEB/디스크 조회)하지 않는다.
86+
* @param started 새로 감지된 인스턴스 (출력)
87+
* @param closed 종료된 인스턴스 (출력)
11388
*/
114-
std::vector<UnityInstance> GetClosedInstances();
89+
void PollChanges(std::vector<UnityInstance>& started, std::vector<UnityInstance>& closed);
11590

11691
/**
11792
* 특정 프로세스 ID가 실행 중인지 확인
11893
* @param processId 프로세스 ID
11994
* @return 실행중이면 true
12095
*/
12196
bool IsProcessRunning(DWORD processId);
122-
123-
/**
124-
* 현재 활성화된 모든 Unity 인스턴스 반환
125-
* @return 현재 실행중인 인스턴스들
126-
*/
127-
const std::unordered_map<DWORD, UnityInstance>& GetActiveInstances() const;
12897
};

include/tray_icon.h

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,20 @@
88

99
// 트레이 아이콘 관련 상수들
1010
#define WM_TRAYICON (WM_USER + 1) // 트레이 아이콘 메시지
11+
#define WM_APP_FILE_EVENT (WM_APP + 1) // 파일 변경 큐 적재 통지 (워커 스레드 → 메인 스레드)
1112
#define IDM_EXIT 100 // 종료 메뉴 ID
1213
#define IDM_SHOW_STATUS 101 // 상태 보기 메뉴 ID
1314
#define IDM_TOGGLE_MONITORING 102 // 모니터링 토글 메뉴 ID
1415
#define IDM_OPEN_DASHBOARD 103 // WakaTime 대시보드 열기
1516
#define IDM_SETTINGS 104 // 설정 메뉴 ID
1617
#define IDM_GITHUB 105 // Github 링크
1718

19+
// 타이머 ID 및 주기 (메시지 펌프 기반 이벤트화)
20+
#define TIMER_PROCESS_SCAN 1 // Unity 프로세스 생성/종료 스캔
21+
#define TIMER_PERIODIC_HEARTBEAT 2 // 포커스 유지 시 주기 heartbeat
22+
#define PROCESS_SCAN_INTERVAL_MS 10000 // 10초
23+
#define PERIODIC_HEARTBEAT_INTERVAL_MS 120000 // 2분
24+
1825
/**
1926
* Windows 시스템 트레이에 아이콘을 표시하고 사용자 인터랙션 처리
2027
*/
@@ -39,6 +46,11 @@ class TrayIcon {
3946
std::function<void()> onOpenDashboard; // 대시보드 열기 콜백
4047
std::function<void()> onShowSettings; // 설정 보기 콜백
4148
std::function<void(const std::string&)> onApiKeyChange; // API 키 변경 콜백
49+
50+
// 이벤트 허브 콜백 (메시지 펌프에서 디스패치)
51+
std::function<void()> onFileEvent; // WM_APP_FILE_EVENT → 파일 이벤트 드레인
52+
std::function<void()> onProcessScan; // TIMER_PROCESS_SCAN → 프로세스 생성/종료 스캔
53+
std::function<void()> onPeriodicTick; // TIMER_PERIODIC_HEARTBEAT → 주기 heartbeat 체크
4254

4355
/**
4456
* 숨겨진 창 생성 (트레이 아이콘 메시지 수신용)
@@ -170,10 +182,17 @@ class TrayIcon {
170182
void Shutdown();
171183

172184
/**
173-
* 메시지 처리 (메인 스레드에서 호출)
174-
* @return 처리된 메시지 수
185+
* 메시지 펌프 실행 (메인 스레드에서 호출, WM_QUIT까지 블록).
186+
* 진입 시 프로세스 스캔/주기 heartbeat 타이머를 설치하고 종료 시 정리한다.
187+
* @return WM_QUIT의 exit code
175188
*/
176-
int ProcessMessages();
189+
int RunMessageLoop();
190+
191+
/**
192+
* 파일 변경 큐 적재를 메인 스레드에 통지 (워커 스레드에서 호출 가능).
193+
* 메시지를 post하여 메시지 펌프가 WM_APP_FILE_EVENT로 깨어나도록 한다.
194+
*/
195+
void NotifyFileEvent();
177196

178197
/**
179198
* 상태 정보 업데이트 (서브메뉴에 반영)
@@ -223,6 +242,21 @@ class TrayIcon {
223242
*/
224243
void SetApiKeyChangeCallback(const std::function<void(const std::string&)> &callback);
225244

245+
/**
246+
* 파일 변경 이벤트 처리 콜백 설정 (WM_APP_FILE_EVENT)
247+
*/
248+
void SetFileEventCallback(const std::function<void()> &callback);
249+
250+
/**
251+
* 프로세스 스캔 콜백 설정 (TIMER_PROCESS_SCAN)
252+
*/
253+
void SetProcessScanCallback(const std::function<void()> &callback);
254+
255+
/**
256+
* 주기 heartbeat 틱 콜백 설정 (TIMER_PERIODIC_HEARTBEAT)
257+
*/
258+
void SetPeriodicTickCallback(const std::function<void()> &callback);
259+
226260
private:
227261
/**
228262
* 정적 윈도우 프로시저 - Windows API 콜백용

include/unity_focus_detector.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ class UnityFocusDetector {
1313

1414
public:
1515
/**
16-
* 포커스 상태 확인
16+
* 포그라운드 창 변경 시 호출 (SetWinEventHook 콜백에서 구동).
17+
* 클래스명에 "Unity"가 포함되면 포커스 전이로 판정한다.
18+
* @param hwnd 새 포그라운드 창 핸들 (nullptr 가능)
1719
*/
18-
void CheckFocused();
20+
void OnForegroundChanged(HWND hwnd);
1921

2022
/**
2123
* 2분마다 호출

main.cpp

Lines changed: 45 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ void OnTrayExit()
5858
{
5959
WT_LOG("[Main] Exit requested from tray");
6060
Globals::RequestExit();
61+
// 메시지 펌프(GetMessage)를 깨워 정상 종료시킨다. 트레이 메뉴 핸들러는
62+
// 메인 스레드(WndProc)에서 동기 호출되므로 여기서 PostQuitMessage가 안전하다.
63+
PostQuitMessage(0);
6164
}
6265

6366
void OnTrayShowStatus()
@@ -241,6 +244,15 @@ void InitialUnityProjectScan()
241244
WT_LOG("[Main] Initial scan complete. Watching " << instances.size() << " Unity projects");
242245
}
243246

247+
// 포그라운드 창 변경 이벤트 콜백 (SetWinEventHook).
248+
// WINEVENT_OUTOFCONTEXT라 메인 스레드의 메시지 펌프 중 디스패치되므로 마샬링 불필요.
249+
void CALLBACK FocusWinEventProc(HWINEVENTHOOK, const DWORD event, const HWND hwnd,
250+
const LONG idObject, LONG, DWORD, DWORD)
251+
{
252+
if (event != EVENT_SYSTEM_FOREGROUND || idObject != OBJID_WINDOW) return;
253+
if (g_unityFocusDetector) g_unityFocusDetector->OnForegroundChanged(hwnd);
254+
}
255+
244256
int main()
245257
{
246258
WT_LOG("[Main] Unity WakaTime Monitor Starting...");
@@ -282,6 +294,11 @@ int main()
282294
g_fileWatcher = &fileWatcher;
283295

284296
fileWatcher.SetChangeCallback(OnFileChanged);
297+
// 워커 스레드의 파일 변경 → 메인 스레드로 PostMessage 마샬링 (InitialScan 이전에 설치)
298+
fileWatcher.SetNotifyCallback([&trayIcon]()
299+
{
300+
trayIcon.NotifyFileEvent();
301+
});
285302

286303
UnityFocusDetector unityFocusDetector;
287304
g_unityFocusDetector = &unityFocusDetector;
@@ -296,43 +313,40 @@ int main()
296313

297314
WT_LOG("\n[Main] Unity WakaTime is now running in background!");
298315

299-
auto lastScan = std::chrono::steady_clock::now();
300-
const auto scanInterval = std::chrono::seconds(10);
301-
302-
while (!Globals::ShouldExit())
316+
// 이벤트 허브 콜백 배선 (TrayIcon의 메시지 펌프에서 디스패치)
317+
trayIcon.SetFileEventCallback([]()
303318
{
304-
if (g_fileWatcher)
305-
{
306-
g_fileWatcher->DrainPendingEvents();
307-
}
319+
if (g_fileWatcher) g_fileWatcher->DrainPendingEvents();
320+
});
308321

309-
if (g_unityFocusDetector) {
310-
g_unityFocusDetector->CheckFocused();
311-
g_unityFocusDetector->SendPeriodicHeartbeat();
312-
}
322+
trayIcon.SetProcessScanCallback([&processMonitor]()
323+
{
324+
std::vector<UnityInstance> started;
325+
std::vector<UnityInstance> closed;
326+
processMonitor.PollChanges(started, closed);
327+
if (!started.empty()) HandleNewUnityInstances(started);
328+
if (!closed.empty()) HandleClosedUnityInstances(closed);
329+
});
330+
331+
trayIcon.SetPeriodicTickCallback([]()
332+
{
333+
if (g_unityFocusDetector) g_unityFocusDetector->SendPeriodicHeartbeat();
334+
});
313335

314-
int msgCount = trayIcon.ProcessMessages();
315-
if (msgCount > 5) std::this_thread::sleep_for(std::chrono::milliseconds(50)); // 많은 메시지 → 빠른 처리
316-
else if (msgCount > 0) std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 일부 메시지 → 보통 처리
317-
else std::this_thread::sleep_for(std::chrono::milliseconds(1000)); // 메시지 없음 → 여유 있게
336+
// 포커스 추적: SetWinEventHook(OUTOFCONTEXT) → 콜백이 메인 펌프에서 디스패치됨 (매초 폴링 제거)
337+
const HWINEVENTHOOK focusHook = SetWinEventHook(
338+
EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND,
339+
nullptr, FocusWinEventProc, 0, 0,
340+
WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS);
318341

319-
if (auto now = std::chrono::steady_clock::now(); now - lastScan >= scanInterval)
320-
{
321-
// 새로운 Unity 인스턴스 감지
322-
if (auto newInstances = processMonitor.GetNewInstances(); !newInstances.empty())
323-
{
324-
HandleNewUnityInstances(newInstances);
325-
}
342+
// 훅 설치 시점에 이미 Unity가 포그라운드일 수 있으므로 초기 상태 1회 캡처
343+
unityFocusDetector.OnForegroundChanged(GetForegroundWindow());
326344

327-
// 종료된 Unity 인스턴스 감지
328-
if (auto closedInstances = processMonitor.GetClosedInstances(); !closedInstances.empty())
329-
{
330-
HandleClosedUnityInstances(closedInstances);
331-
}
345+
// 메시지 펌프: idle 시 GetMessage가 커널에서 블록되어 CPU ≈ 0,
346+
// 파일/포커스/타이머/트레이 이벤트에만 깨어난다. WM_QUIT까지 블록.
347+
trayIcon.RunMessageLoop();
332348

333-
lastScan = now;
334-
}
335-
}
349+
if (focusHook) UnhookWinEvent(focusHook);
336350

337351
WT_LOG("\n[Main] Shutting down Unity WakaTime...");
338352
trayIcon.ShowInfoNotification("Unity WakaTime shutting down...");

0 commit comments

Comments
 (0)