com.kylin.di 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +22 -0
- package/CHANGELOG.md +17 -0
- package/CLAUDE.md +58 -0
- package/LICENSE +21 -0
- package/README.md +704 -0
- package/Runtime/Attributes/InjectAttribute.cs +11 -0
- package/Runtime/Attributes/InjectAttribute.cs.meta +2 -0
- package/Runtime/Attributes/ViewModelAttribute.cs +17 -0
- package/Runtime/Attributes/ViewModelAttribute.cs.meta +2 -0
- package/Runtime/Attributes.meta +8 -0
- package/Runtime/Core/DIBehaviour.cs +70 -0
- package/Runtime/Core/DIBehaviour.cs.meta +2 -0
- package/Runtime/Core/LifetimeScope.cs +264 -0
- package/Runtime/Core/LifetimeScope.cs.meta +2 -0
- package/Runtime/Core.meta +8 -0
- package/Runtime/DI/DependencyBuilder.cs +114 -0
- package/Runtime/DI/DependencyBuilder.cs.meta +2 -0
- package/Runtime/DI/DependencyInjector.cs +104 -0
- package/Runtime/DI/DependencyInjector.cs.meta +2 -0
- package/Runtime/DI/InstanceFactory.cs +36 -0
- package/Runtime/DI/InstanceFactory.cs.meta +2 -0
- package/Runtime/DI/KDI.cs +54 -0
- package/Runtime/DI/KDI.cs.meta +2 -0
- package/Runtime/DI/Registration.cs +29 -0
- package/Runtime/DI/Registration.cs.meta +2 -0
- package/Runtime/DI/Scope.cs +183 -0
- package/Runtime/DI/Scope.cs.meta +2 -0
- package/Runtime/DI/ScopeBuilder.cs +68 -0
- package/Runtime/DI/ScopeBuilder.cs.meta +2 -0
- package/Runtime/DI/ScopeExtensions.cs +70 -0
- package/Runtime/DI/ScopeExtensions.cs.meta +2 -0
- package/Runtime/DI.meta +8 -0
- package/Runtime/Debug/ClosureAnalyzer.cs +377 -0
- package/Runtime/Debug/ClosureAnalyzer.cs.meta +2 -0
- package/Runtime/Debug/ClosureProfilerWindow.cs +435 -0
- package/Runtime/Debug/ClosureProfilerWindow.cs.meta +2 -0
- package/Runtime/Debug/SubscriberInfo.cs +661 -0
- package/Runtime/Debug/SubscriberInfo.cs.meta +3 -0
- package/Runtime/Debug.meta +3 -0
- package/Runtime/Kylin.DI.asmdef +14 -0
- package/Runtime/Kylin.DI.asmdef.meta +7 -0
- package/Runtime/SubscribableProperty/Reaction.cs +61 -0
- package/Runtime/SubscribableProperty/Reaction.cs.meta +2 -0
- package/Runtime/SubscribableProperty/SubscribableCollection.cs +325 -0
- package/Runtime/SubscribableProperty/SubscribableCollection.cs.meta +3 -0
- package/Runtime/SubscribableProperty/SubscribableCollectionExtensions.cs +24 -0
- package/Runtime/SubscribableProperty/SubscribableCollectionExtensions.cs.meta +3 -0
- package/Runtime/SubscribableProperty/SubscribableCommand.cs +52 -0
- package/Runtime/SubscribableProperty/SubscribableCommand.cs.meta +2 -0
- package/Runtime/SubscribableProperty/SubscribableDictionary.cs +350 -0
- package/Runtime/SubscribableProperty/SubscribableDictionary.cs.meta +3 -0
- package/Runtime/SubscribableProperty/SubscribableProperty.cs +119 -0
- package/Runtime/SubscribableProperty/SubscribableProperty.cs.meta +2 -0
- package/Runtime/SubscribableProperty/SubscribablePropertyExtensions.cs +39 -0
- package/Runtime/SubscribableProperty/SubscribablePropertyExtensions.cs.meta +3 -0
- package/Runtime/SubscribableProperty/SubscribablePropertyLinq.cs +86 -0
- package/Runtime/SubscribableProperty/SubscribablePropertyLinq.cs.meta +2 -0
- package/Runtime/SubscribableProperty.meta +8 -0
- package/Runtime/Update/IFixedUpdatable.cs +16 -0
- package/Runtime/Update/IFixedUpdatable.cs.meta +2 -0
- package/Runtime/Update/ILateUpdatable.cs +16 -0
- package/Runtime/Update/ILateUpdatable.cs.meta +2 -0
- package/Runtime/Update/IUpdatable.cs +16 -0
- package/Runtime/Update/IUpdatable.cs.meta +2 -0
- package/Runtime/Update/IUpdatePriority.cs +15 -0
- package/Runtime/Update/IUpdatePriority.cs.meta +2 -0
- package/Runtime/Update/UpdateLoopManager.cs +240 -0
- package/Runtime/Update/UpdateLoopManager.cs.meta +2 -0
- package/Runtime/Update.meta +8 -0
- package/Runtime.meta +8 -0
- package/package.json +19 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
#if UNITY_EDITOR
|
|
2
|
+
using System;
|
|
3
|
+
using System.Collections.Generic;
|
|
4
|
+
using System.Linq;
|
|
5
|
+
using UnityEditor;
|
|
6
|
+
using UnityEngine;
|
|
7
|
+
|
|
8
|
+
namespace Kylin.SubscribableProperty
|
|
9
|
+
{
|
|
10
|
+
public class ClosureProfilerWindow : EditorWindow
|
|
11
|
+
{
|
|
12
|
+
private static ClosureProfilerWindow _instance;
|
|
13
|
+
private static bool _isProfilingEnabled = false;
|
|
14
|
+
|
|
15
|
+
private Vector2 _scrollPosition;
|
|
16
|
+
private readonly Dictionary<ClosureCaptureInfo, bool> _foldoutStates = new Dictionary<ClosureCaptureInfo, bool>();
|
|
17
|
+
|
|
18
|
+
private string _searchFilter = "";
|
|
19
|
+
private bool _showOnlyActive = true;
|
|
20
|
+
private bool _showUnsubscribed = true;
|
|
21
|
+
private ClosureCaptureRisk _minRiskFilter = ClosureCaptureRisk.Low;
|
|
22
|
+
private bool _autoRefresh = true;
|
|
23
|
+
private double _lastRefreshTime;
|
|
24
|
+
|
|
25
|
+
private enum SortMode { Time, Memory, Risk, Lifetime }
|
|
26
|
+
private SortMode _sortMode = SortMode.Risk;
|
|
27
|
+
private bool _sortDescending = true;
|
|
28
|
+
|
|
29
|
+
public static bool IsWindowOpen => _instance != null;
|
|
30
|
+
public static bool IsProfilingEnabled => _isProfilingEnabled;
|
|
31
|
+
|
|
32
|
+
[MenuItem("Tools/Closure Profiler")]
|
|
33
|
+
public static void ShowWindow()
|
|
34
|
+
{
|
|
35
|
+
_instance = GetWindow<ClosureProfilerWindow>("클로저 프로파일러");
|
|
36
|
+
_instance.minSize = new Vector2(800, 600);
|
|
37
|
+
_instance.Show();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private void OnEnable()
|
|
41
|
+
{
|
|
42
|
+
_instance = this;
|
|
43
|
+
EditorApplication.update += OnEditorUpdate;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private void OnDisable()
|
|
47
|
+
{
|
|
48
|
+
_instance = null;
|
|
49
|
+
EditorApplication.update -= OnEditorUpdate;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private void OnEditorUpdate()
|
|
53
|
+
{
|
|
54
|
+
if (_autoRefresh && EditorApplication.timeSinceStartup - _lastRefreshTime > 1.0)
|
|
55
|
+
{
|
|
56
|
+
_lastRefreshTime = EditorApplication.timeSinceStartup;
|
|
57
|
+
ClosureProfiler.RefreshMemoryInfo();
|
|
58
|
+
Repaint();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private void OnGUI()
|
|
63
|
+
{
|
|
64
|
+
DrawHeader();
|
|
65
|
+
DrawStatistics();
|
|
66
|
+
DrawFilters();
|
|
67
|
+
DrawClosureList();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private void DrawHeader()
|
|
71
|
+
{
|
|
72
|
+
EditorGUILayout.BeginVertical("Box");
|
|
73
|
+
|
|
74
|
+
EditorGUILayout.LabelField("클로저 캡처 프로파일러", EditorStyles.boldLabel);
|
|
75
|
+
EditorGUILayout.LabelField("람다식의 외부 변수 캡처를 감지하고 메모리 누수를 분석합니다.", EditorStyles.miniLabel);
|
|
76
|
+
|
|
77
|
+
EditorGUILayout.Space(5);
|
|
78
|
+
|
|
79
|
+
EditorGUI.BeginDisabledGroup(Application.isPlaying);
|
|
80
|
+
bool newProfilingEnabled = EditorGUILayout.Toggle("프로파일링 활성화", _isProfilingEnabled);
|
|
81
|
+
if (newProfilingEnabled != _isProfilingEnabled)
|
|
82
|
+
{
|
|
83
|
+
_isProfilingEnabled = newProfilingEnabled;
|
|
84
|
+
if (!_isProfilingEnabled)
|
|
85
|
+
{
|
|
86
|
+
ClosureProfiler.Clear();
|
|
87
|
+
_foldoutStates.Clear();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
EditorGUI.EndDisabledGroup();
|
|
91
|
+
|
|
92
|
+
_autoRefresh = EditorGUILayout.Toggle("자동 새로고침", _autoRefresh);
|
|
93
|
+
|
|
94
|
+
if (Application.isPlaying && _isProfilingEnabled)
|
|
95
|
+
{
|
|
96
|
+
EditorGUILayout.HelpBox("프로파일링 활성화됨 - 클로저 캡처를 모니터링 중입니다.", MessageType.Info);
|
|
97
|
+
}
|
|
98
|
+
else if (Application.isPlaying)
|
|
99
|
+
{
|
|
100
|
+
EditorGUILayout.HelpBox("플레이 모드 진입 전에 프로파일링을 활성화하세요.", MessageType.Warning);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
EditorGUILayout.EndVertical();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private void DrawStatistics()
|
|
107
|
+
{
|
|
108
|
+
EditorGUILayout.BeginVertical("Box");
|
|
109
|
+
EditorGUILayout.LabelField("통계", EditorStyles.boldLabel);
|
|
110
|
+
|
|
111
|
+
var (total, active, unsubscribed, totalMemory) = ClosureProfiler.GetStatistics();
|
|
112
|
+
|
|
113
|
+
EditorGUILayout.BeginHorizontal();
|
|
114
|
+
EditorGUILayout.LabelField($"전체 클로저: {total}", GUILayout.Width(150));
|
|
115
|
+
EditorGUILayout.LabelField($"활성: {active}", GUILayout.Width(100));
|
|
116
|
+
EditorGUILayout.LabelField($"구독 해제됨: {unsubscribed}", GUILayout.Width(150));
|
|
117
|
+
EditorGUILayout.EndHorizontal();
|
|
118
|
+
|
|
119
|
+
EditorGUILayout.BeginHorizontal();
|
|
120
|
+
var memoryKB = totalMemory / 1024f;
|
|
121
|
+
var memoryMB = memoryKB / 1024f;
|
|
122
|
+
var memoryText = memoryMB >= 1f ? $"{memoryMB:F2} MB" : $"{memoryKB:F2} KB";
|
|
123
|
+
|
|
124
|
+
var prevColor = GUI.contentColor;
|
|
125
|
+
if (memoryMB >= 10f)
|
|
126
|
+
GUI.contentColor = Color.red;
|
|
127
|
+
else if (memoryMB >= 1f)
|
|
128
|
+
GUI.contentColor = Color.yellow;
|
|
129
|
+
else
|
|
130
|
+
GUI.contentColor = Color.green;
|
|
131
|
+
|
|
132
|
+
EditorGUILayout.LabelField($"추정 메모리 사용량: {memoryText}", EditorStyles.boldLabel);
|
|
133
|
+
GUI.contentColor = prevColor;
|
|
134
|
+
EditorGUILayout.EndHorizontal();
|
|
135
|
+
|
|
136
|
+
EditorGUILayout.EndVertical();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private void DrawFilters()
|
|
140
|
+
{
|
|
141
|
+
EditorGUILayout.BeginVertical("Box");
|
|
142
|
+
EditorGUILayout.LabelField("필터 및 정렬", EditorStyles.boldLabel);
|
|
143
|
+
|
|
144
|
+
EditorGUILayout.BeginHorizontal();
|
|
145
|
+
_searchFilter = EditorGUILayout.TextField("검색", _searchFilter);
|
|
146
|
+
_showOnlyActive = EditorGUILayout.Toggle("활성만", _showOnlyActive, GUILayout.Width(100));
|
|
147
|
+
_showUnsubscribed = EditorGUILayout.Toggle("구독 해제 포함", _showUnsubscribed, GUILayout.Width(120));
|
|
148
|
+
EditorGUILayout.EndHorizontal();
|
|
149
|
+
|
|
150
|
+
EditorGUILayout.BeginHorizontal();
|
|
151
|
+
EditorGUILayout.LabelField("최소 위험도", GUILayout.Width(100));
|
|
152
|
+
_minRiskFilter = (ClosureCaptureRisk)EditorGUILayout.EnumPopup(_minRiskFilter, GUILayout.Width(150));
|
|
153
|
+
|
|
154
|
+
EditorGUILayout.LabelField("정렬", GUILayout.Width(50));
|
|
155
|
+
_sortMode = (SortMode)EditorGUILayout.EnumPopup(_sortMode, GUILayout.Width(100));
|
|
156
|
+
_sortDescending = EditorGUILayout.Toggle("↓", _sortDescending, GUILayout.Width(30));
|
|
157
|
+
EditorGUILayout.EndHorizontal();
|
|
158
|
+
|
|
159
|
+
EditorGUILayout.BeginHorizontal();
|
|
160
|
+
if (GUILayout.Button("새로고침"))
|
|
161
|
+
{
|
|
162
|
+
ClosureProfiler.RefreshMemoryInfo();
|
|
163
|
+
Repaint();
|
|
164
|
+
}
|
|
165
|
+
if (GUILayout.Button("모두 지우기"))
|
|
166
|
+
{
|
|
167
|
+
ClosureProfiler.Clear();
|
|
168
|
+
_foldoutStates.Clear();
|
|
169
|
+
}
|
|
170
|
+
if (GUILayout.Button("강제 GC"))
|
|
171
|
+
{
|
|
172
|
+
System.GC.Collect();
|
|
173
|
+
System.GC.WaitForPendingFinalizers();
|
|
174
|
+
System.GC.Collect();
|
|
175
|
+
ClosureProfiler.RefreshMemoryInfo();
|
|
176
|
+
}
|
|
177
|
+
if (GUILayout.Button("CSV 내보내기"))
|
|
178
|
+
{
|
|
179
|
+
ExportToCSV();
|
|
180
|
+
}
|
|
181
|
+
EditorGUILayout.EndHorizontal();
|
|
182
|
+
|
|
183
|
+
EditorGUILayout.EndVertical();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private void DrawClosureList()
|
|
187
|
+
{
|
|
188
|
+
if (!_isProfilingEnabled)
|
|
189
|
+
{
|
|
190
|
+
EditorGUILayout.HelpBox("프로파일링을 시작하려면 위에서 활성화하세요.", MessageType.Info);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
var captures = ClosureProfiler.GetAllCaptures()
|
|
195
|
+
.Where(c => FilterCapture(c))
|
|
196
|
+
.OrderBy(c => GetSortKey(c), _sortDescending)
|
|
197
|
+
.ToList();
|
|
198
|
+
|
|
199
|
+
EditorGUILayout.LabelField($"클로저 캡처: {captures.Count}개", EditorStyles.boldLabel);
|
|
200
|
+
|
|
201
|
+
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
|
|
202
|
+
|
|
203
|
+
foreach (var capture in captures)
|
|
204
|
+
{
|
|
205
|
+
DrawCaptureInfo(capture);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
EditorGUILayout.EndScrollView();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private bool FilterCapture(ClosureCaptureInfo capture)
|
|
212
|
+
{
|
|
213
|
+
if (_showOnlyActive && !capture.IsAlive)
|
|
214
|
+
return false;
|
|
215
|
+
|
|
216
|
+
if (!_showUnsubscribed && capture.IsUnsubscribed)
|
|
217
|
+
return false;
|
|
218
|
+
|
|
219
|
+
var maxRisk = capture.CapturedVariables.Max(v => v.RiskLevel);
|
|
220
|
+
if (maxRisk < _minRiskFilter)
|
|
221
|
+
return false;
|
|
222
|
+
|
|
223
|
+
if (!string.IsNullOrEmpty(_searchFilter))
|
|
224
|
+
{
|
|
225
|
+
var filter = _searchFilter.ToLower();
|
|
226
|
+
if (!capture.ClosureTypeName.ToLower().Contains(filter) &&
|
|
227
|
+
!capture.CaptureLocation.ToLower().Contains(filter))
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private IComparable GetSortKey(ClosureCaptureInfo capture)
|
|
235
|
+
{
|
|
236
|
+
return _sortMode switch
|
|
237
|
+
{
|
|
238
|
+
SortMode.Time => _sortDescending ? -capture.CaptureTime.Ticks : capture.CaptureTime.Ticks,
|
|
239
|
+
SortMode.Memory => _sortDescending ? -capture.EstimatedMemoryBytes : capture.EstimatedMemoryBytes,
|
|
240
|
+
SortMode.Risk => _sortDescending
|
|
241
|
+
? -(int)capture.CapturedVariables.Max(v => v.RiskLevel)
|
|
242
|
+
: (int)capture.CapturedVariables.Max(v => v.RiskLevel),
|
|
243
|
+
SortMode.Lifetime => _sortDescending
|
|
244
|
+
? -(capture.Lifetime?.TotalSeconds ?? 0)
|
|
245
|
+
: (capture.Lifetime?.TotalSeconds ?? 0),
|
|
246
|
+
_ => 0
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private void DrawCaptureInfo(ClosureCaptureInfo capture)
|
|
251
|
+
{
|
|
252
|
+
if (!_foldoutStates.ContainsKey(capture))
|
|
253
|
+
_foldoutStates[capture] = false;
|
|
254
|
+
|
|
255
|
+
EditorGUILayout.BeginVertical("Box");
|
|
256
|
+
|
|
257
|
+
// 헤더
|
|
258
|
+
EditorGUILayout.BeginHorizontal();
|
|
259
|
+
|
|
260
|
+
var maxRisk = capture.CapturedVariables.Any()
|
|
261
|
+
? capture.CapturedVariables.Max(v => v.RiskLevel)
|
|
262
|
+
: ClosureCaptureRisk.Low;
|
|
263
|
+
|
|
264
|
+
var riskIcon = GetRiskIcon(maxRisk);
|
|
265
|
+
var memoryKB = capture.EstimatedMemoryBytes / 1024f;
|
|
266
|
+
var headerText = $"{riskIcon} {capture.ClosureTypeName} ({memoryKB:F2}KB)";
|
|
267
|
+
|
|
268
|
+
if (capture.IsUnsubscribed)
|
|
269
|
+
{
|
|
270
|
+
headerText += " [구독 해제됨]";
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
var prevColor = GUI.color;
|
|
274
|
+
GUI.color = GetRiskColor(maxRisk);
|
|
275
|
+
|
|
276
|
+
_foldoutStates[capture] = EditorGUILayout.Foldout(_foldoutStates[capture], headerText, true);
|
|
277
|
+
|
|
278
|
+
GUI.color = prevColor;
|
|
279
|
+
|
|
280
|
+
if (!capture.IsAlive)
|
|
281
|
+
{
|
|
282
|
+
EditorGUILayout.LabelField("[GC됨]", EditorStyles.miniLabel, GUILayout.Width(60));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
EditorGUILayout.EndHorizontal();
|
|
286
|
+
|
|
287
|
+
// 상세 정보
|
|
288
|
+
if (_foldoutStates[capture])
|
|
289
|
+
{
|
|
290
|
+
EditorGUI.indentLevel++;
|
|
291
|
+
|
|
292
|
+
EditorGUILayout.LabelField("위치:", capture.CaptureLocation);
|
|
293
|
+
EditorGUILayout.LabelField("캡처 시간:", capture.CaptureTime.ToString("HH:mm:ss"));
|
|
294
|
+
|
|
295
|
+
if (capture.IsUnsubscribed)
|
|
296
|
+
{
|
|
297
|
+
EditorGUILayout.LabelField("구독 해제 시간:", capture.UnsubscribeTime?.ToString("HH:mm:ss") ?? "Unknown");
|
|
298
|
+
EditorGUILayout.LabelField("생존 시간:", $"{capture.Lifetime?.TotalSeconds:F2}초");
|
|
299
|
+
}
|
|
300
|
+
else
|
|
301
|
+
{
|
|
302
|
+
EditorGUILayout.LabelField("현재 생존 시간:", $"{capture.Lifetime?.TotalSeconds:F2}초");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
EditorGUILayout.Space(5);
|
|
306
|
+
EditorGUILayout.LabelField("캡처된 변수:", EditorStyles.boldLabel);
|
|
307
|
+
|
|
308
|
+
EditorGUI.indentLevel++;
|
|
309
|
+
foreach (var variable in capture.CapturedVariables.OrderByDescending(v => v.RiskLevel))
|
|
310
|
+
{
|
|
311
|
+
DrawVariableInfo(variable);
|
|
312
|
+
}
|
|
313
|
+
EditorGUI.indentLevel--;
|
|
314
|
+
|
|
315
|
+
EditorGUI.indentLevel--;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
EditorGUILayout.EndVertical();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private void DrawVariableInfo(CapturedVariableInfo variable)
|
|
322
|
+
{
|
|
323
|
+
EditorGUILayout.BeginHorizontal("Box");
|
|
324
|
+
|
|
325
|
+
var riskIcon = GetRiskIcon(variable.RiskLevel);
|
|
326
|
+
var memoryBytes = variable.EstimatedMemoryBytes;
|
|
327
|
+
var memoryText = memoryBytes >= 1024 ? $"{memoryBytes / 1024f:F2}KB" : $"{memoryBytes}B";
|
|
328
|
+
|
|
329
|
+
var prevColor = GUI.contentColor;
|
|
330
|
+
GUI.contentColor = GetRiskColor(variable.RiskLevel);
|
|
331
|
+
|
|
332
|
+
EditorGUILayout.LabelField($"{riskIcon} {variable.FieldName}", GUILayout.Width(150));
|
|
333
|
+
EditorGUILayout.LabelField(variable.FieldTypeName, GUILayout.Width(150));
|
|
334
|
+
EditorGUILayout.LabelField(memoryText, GUILayout.Width(80));
|
|
335
|
+
|
|
336
|
+
if (!variable.IsAlive && variable.IsReferenceType)
|
|
337
|
+
{
|
|
338
|
+
EditorGUILayout.LabelField("[GC됨]", EditorStyles.miniLabel, GUILayout.Width(60));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
GUI.contentColor = prevColor;
|
|
342
|
+
|
|
343
|
+
EditorGUILayout.EndHorizontal();
|
|
344
|
+
|
|
345
|
+
if (variable.RiskLevel >= ClosureCaptureRisk.Medium)
|
|
346
|
+
{
|
|
347
|
+
EditorGUI.indentLevel++;
|
|
348
|
+
EditorGUILayout.LabelField($"⚠ {variable.RiskDescription}", EditorStyles.helpBox);
|
|
349
|
+
EditorGUI.indentLevel--;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private string GetRiskIcon(ClosureCaptureRisk risk)
|
|
354
|
+
{
|
|
355
|
+
return risk switch
|
|
356
|
+
{
|
|
357
|
+
ClosureCaptureRisk.Critical => "🔴",
|
|
358
|
+
ClosureCaptureRisk.High => "🟠",
|
|
359
|
+
ClosureCaptureRisk.Medium => "🟡",
|
|
360
|
+
ClosureCaptureRisk.Low => "🟢",
|
|
361
|
+
_ => "⚪"
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private Color GetRiskColor(ClosureCaptureRisk risk)
|
|
366
|
+
{
|
|
367
|
+
return risk switch
|
|
368
|
+
{
|
|
369
|
+
ClosureCaptureRisk.Critical => new Color(1f, 0.3f, 0.3f),
|
|
370
|
+
ClosureCaptureRisk.High => new Color(1f, 0.6f, 0.2f),
|
|
371
|
+
ClosureCaptureRisk.Medium => new Color(1f, 1f, 0.4f),
|
|
372
|
+
ClosureCaptureRisk.Low => new Color(0.6f, 1f, 0.6f),
|
|
373
|
+
_ => Color.white
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/// <summary>
|
|
378
|
+
/// 분석용..
|
|
379
|
+
/// </summary>
|
|
380
|
+
private void ExportToCSV()
|
|
381
|
+
{
|
|
382
|
+
var path = EditorUtility.SaveFilePanel("CSV 내보내기", "", "closure_profile.csv", "csv");
|
|
383
|
+
if (string.IsNullOrEmpty(path))
|
|
384
|
+
return;
|
|
385
|
+
|
|
386
|
+
try
|
|
387
|
+
{
|
|
388
|
+
var lines = new List<string>
|
|
389
|
+
{
|
|
390
|
+
"Capture Time,Location,Closure Type,Is Active,Memory (KB),Max Risk,Variables,Lifetime (sec)"
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
foreach (var capture in ClosureProfiler.GetAllCaptures())
|
|
394
|
+
{
|
|
395
|
+
var maxRisk = capture.CapturedVariables.Any()
|
|
396
|
+
? capture.CapturedVariables.Max(v => v.RiskLevel)
|
|
397
|
+
: ClosureCaptureRisk.Low;
|
|
398
|
+
|
|
399
|
+
var line = $"{capture.CaptureTime:yyyy-MM-dd HH:mm:ss}," +
|
|
400
|
+
$"\"{capture.CaptureLocation}\"," +
|
|
401
|
+
$"{capture.ClosureTypeName}," +
|
|
402
|
+
$"{(!capture.IsUnsubscribed && capture.IsAlive)}," +
|
|
403
|
+
$"{capture.EstimatedMemoryBytes / 1024f:F2}," +
|
|
404
|
+
$"{maxRisk}," +
|
|
405
|
+
$"{capture.CapturedVariables.Count}," +
|
|
406
|
+
$"{capture.Lifetime?.TotalSeconds:F2}";
|
|
407
|
+
|
|
408
|
+
lines.Add(line);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
System.IO.File.WriteAllLines(path, lines);
|
|
412
|
+
EditorUtility.DisplayDialog("내보내기 완료", $"CSV 파일이 저장되었습니다:\n{path}", "확인");
|
|
413
|
+
}
|
|
414
|
+
catch (Exception ex)
|
|
415
|
+
{
|
|
416
|
+
EditorUtility.DisplayDialog("오류", $"CSV 내보내기 실패:\n{ex.Message}", "확인");
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// OrderBy 확장 메서드 (정렬 방향 지원)
|
|
422
|
+
internal static class ClosureProfilerExtensions
|
|
423
|
+
{
|
|
424
|
+
public static IOrderedEnumerable<T> OrderBy<T, TKey>(
|
|
425
|
+
this IEnumerable<T> source,
|
|
426
|
+
Func<T, TKey> keySelector,
|
|
427
|
+
bool descending)
|
|
428
|
+
{
|
|
429
|
+
return descending
|
|
430
|
+
? source.OrderByDescending(keySelector)
|
|
431
|
+
: source.OrderBy(keySelector);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
#endif
|