Skip to content

Commit f30432f

Browse files
committed
Scene Traversal 최적화:
Dictionary<string, object> 대신 경량 DTO HierarchyNode를 사용하여 가비지 컬렉션(GC) 부하를 줄였습니다. maxDepth 파라미터를 추가하여(기본값 20), 무한히 깊은 계층 구조로 인한 프리징을 방지했습니다. Asset Search 최적화: Lazy Loading: 메타데이터만 필요한 경우, 무거운 에셋 객체(UnityEngine.Object)를 로드하지 않도록 개선하여 메모리 사용량을 대폭 절감했습니다. Early Filtering: 검색 결과 전체를 로드하고 필터링하던 방식에서, 경로만 먼저 필터링하고 페이지네이션에 해당하는 항목만 로드하도록 변경했습니다. (수천 개 에셋 검색 시 속도 향상) Reflection Caching: AssetService에 PropertyInfo/FieldInfo 캐시를 도입하여, 반복적인 리플렉션 탐색 비용을 제거했습니다.
1 parent 0156b1e commit f30432f

File tree

3 files changed

+145
-88
lines changed

3 files changed

+145
-88
lines changed

MCPForUnity/Editor/Services/AssetService.cs

Lines changed: 74 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -458,26 +458,36 @@ public static object SearchAssets(JObject @params)
458458
try
459459
{
460460
string[] guids = AssetDatabase.FindAssets(string.Join(" ", searchFilters), folderScope);
461-
List<object> results = new List<object>();
461+
462+
// Optimization: Filter paths first without creating full data objects
463+
List<string> matchedPaths = new List<string>();
462464
int totalFound = 0;
463465

464466
foreach (string guid in guids)
465467
{
466468
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
467469
if (string.IsNullOrEmpty(assetPath)) continue;
468470

471+
// I/O Check only if filter is active
469472
if (filterDateAfter.HasValue)
470473
{
471474
DateTime lastWriteTime = File.GetLastWriteTimeUtc(Path.Combine(Directory.GetCurrentDirectory(), assetPath));
472475
if (lastWriteTime <= filterDateAfter.Value) continue;
473476
}
474477

478+
matchedPaths.Add(assetPath);
475479
totalFound++;
476-
results.Add(GetAssetData(assetPath, generatePreview));
477480
}
478481

482+
// Optimization: Page BEFORE fetching heavy data
479483
int startIndex = (pageNumber - 1) * pageSize;
480-
var pagedResults = results.Skip(startIndex).Take(pageSize).ToList();
484+
var pagedPaths = matchedPaths.Skip(startIndex).Take(pageSize);
485+
486+
List<object> pagedResults = new List<object>();
487+
foreach (var path in pagedPaths)
488+
{
489+
pagedResults.Add(GetAssetData(path, generatePreview));
490+
}
481491

482492
return new SuccessResponse(
483493
$"Found {totalFound} asset(s). Returning page {pageNumber} ({pagedResults.Count} assets).",
@@ -606,27 +616,45 @@ private static bool ApplyObjectProperties(UnityEngine.Object target, JObject pro
606616
return modified;
607617
}
608618

619+
// --- Reflection Cache ---
620+
private static readonly Dictionary<(Type, string), System.Reflection.PropertyInfo> _propertyCache = new Dictionary<(Type, string), System.Reflection.PropertyInfo>();
621+
private static readonly Dictionary<(Type, string), System.Reflection.FieldInfo> _fieldCache = new Dictionary<(Type, string), System.Reflection.FieldInfo>();
622+
609623
private static bool SetPropertyOrField(object target, string memberName, JToken value, Type type)
610624
{
611625
type = type ?? target.GetType();
612-
System.Reflection.BindingFlags flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.IgnoreCase;
613-
626+
// System.Reflection.BindingFlags flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.IgnoreCase;
627+
// Note: Caching logic assumes flags don't change.
628+
614629
try {
615-
System.Reflection.PropertyInfo propInfo = type.GetProperty(memberName, flags);
630+
// Try Property Cache
631+
if (!_propertyCache.TryGetValue((type, memberName), out var propInfo))
632+
{
633+
propInfo = type.GetProperty(memberName, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.IgnoreCase);
634+
if (propInfo != null) _propertyCache[(type, memberName)] = propInfo;
635+
}
636+
616637
if (propInfo != null && propInfo.CanWrite) {
617638
object val = ConvertJTokenToType(value, propInfo.PropertyType);
618639
if (val != null && !object.Equals(propInfo.GetValue(target), val)) {
619640
propInfo.SetValue(target, val);
620641
return true;
621642
}
622-
} else {
623-
System.Reflection.FieldInfo fieldInfo = type.GetField(memberName, flags);
624-
if (fieldInfo != null) {
625-
object val = ConvertJTokenToType(value, fieldInfo.FieldType);
626-
if (val != null && !object.Equals(fieldInfo.GetValue(target), val)) {
627-
fieldInfo.SetValue(target, val);
628-
return true;
629-
}
643+
return false; // Found but didn't change or couldn't convert
644+
}
645+
646+
// Try Field Cache
647+
if (!_fieldCache.TryGetValue((type, memberName), out var fieldInfo))
648+
{
649+
fieldInfo = type.GetField(memberName, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.IgnoreCase);
650+
if (fieldInfo != null) _fieldCache[(type, memberName)] = fieldInfo;
651+
}
652+
653+
if (fieldInfo != null) {
654+
object val = ConvertJTokenToType(value, fieldInfo.FieldType);
655+
if (val != null && !object.Equals(fieldInfo.GetValue(target), val)) {
656+
fieldInfo.SetValue(target, val);
657+
return true;
630658
}
631659
}
632660
} catch (Exception ex) {
@@ -680,35 +708,42 @@ private static object GetAssetData(string path, bool generatePreview = false)
680708

681709
string guid = AssetDatabase.AssetPathToGUID(path);
682710
Type assetType = AssetDatabase.GetMainAssetTypeAtPath(path);
683-
UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path);
711+
712+
// Lazy Load: Only load object if preview is requested
713+
UnityEngine.Object asset = null;
684714
string previewBase64 = null;
685715
int previewWidth = 0;
686716
int previewHeight = 0;
717+
int instanceID = 0;
687718

688-
if (generatePreview && asset != null)
719+
if (generatePreview)
689720
{
690-
Texture2D preview = AssetPreview.GetAssetPreview(asset);
691-
if (preview != null) {
692-
try {
693-
// (Preview generation logic omitted for brevity in summary, assume same logic)
694-
RenderTexture rt = RenderTexture.GetTemporary(preview.width, preview.height);
695-
Graphics.Blit(preview, rt);
696-
RenderTexture previous = RenderTexture.active;
697-
RenderTexture.active = rt;
698-
Texture2D readablePreview = new Texture2D(preview.width, preview.height, TextureFormat.RGB24, false);
699-
readablePreview.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0);
700-
readablePreview.Apply();
701-
RenderTexture.active = previous;
702-
RenderTexture.ReleaseTemporary(rt);
703-
704-
byte[] pngData = readablePreview.EncodeToPNG();
705-
if (pngData != null) {
706-
previewBase64 = Convert.ToBase64String(pngData);
707-
previewWidth = readablePreview.width;
708-
previewHeight = readablePreview.height;
709-
}
710-
UnityEngine.Object.DestroyImmediate(readablePreview);
711-
} catch {}
721+
asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path);
722+
if (asset != null)
723+
{
724+
instanceID = asset.GetInstanceID();
725+
Texture2D preview = AssetPreview.GetAssetPreview(asset);
726+
if (preview != null) {
727+
try {
728+
RenderTexture rt = RenderTexture.GetTemporary(preview.width, preview.height);
729+
Graphics.Blit(preview, rt);
730+
RenderTexture previous = RenderTexture.active;
731+
RenderTexture.active = rt;
732+
Texture2D readablePreview = new Texture2D(preview.width, preview.height, TextureFormat.RGB24, false);
733+
readablePreview.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0);
734+
readablePreview.Apply();
735+
RenderTexture.active = previous;
736+
RenderTexture.ReleaseTemporary(rt);
737+
738+
byte[] pngData = readablePreview.EncodeToPNG();
739+
if (pngData != null) {
740+
previewBase64 = Convert.ToBase64String(pngData);
741+
previewWidth = readablePreview.width;
742+
previewHeight = readablePreview.height;
743+
}
744+
UnityEngine.Object.DestroyImmediate(readablePreview);
745+
} catch {}
746+
}
712747
}
713748
}
714749

@@ -720,7 +755,7 @@ private static object GetAssetData(string path, bool generatePreview = false)
720755
name = Path.GetFileNameWithoutExtension(path),
721756
fileName = Path.GetFileName(path),
722757
isFolder = AssetDatabase.IsValidFolder(path),
723-
instanceID = asset?.GetInstanceID() ?? 0,
758+
instanceID = instanceID, // 0 if not loaded
724759
lastWriteTimeUtc = File.GetLastWriteTimeUtc(Path.Combine(Directory.GetCurrentDirectory(), path)).ToString("o"),
725760
previewBase64 = previewBase64,
726761
previewWidth = previewWidth,

MCPForUnity/Editor/Services/SceneTraversalService.cs

Lines changed: 65 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -10,68 +10,88 @@ namespace MCPForUnity.Editor.Services
1010
/// </summary>
1111
public static class SceneTraversalService
1212
{
13-
public static List<object> GetSceneHierarchyData(Scene scene)
13+
[System.Serializable]
14+
public class HierarchyNode
15+
{
16+
public string name;
17+
public bool activeSelf;
18+
public bool activeInHierarchy;
19+
public string tag;
20+
public int layer;
21+
public bool isStatic;
22+
public int instanceID;
23+
public TransformData transform;
24+
public List<HierarchyNode> children;
25+
}
26+
27+
[System.Serializable]
28+
public class TransformData
29+
{
30+
public Vector3Data position;
31+
public Vector3Data rotation;
32+
public Vector3Data scale;
33+
}
34+
35+
[System.Serializable]
36+
public struct Vector3Data
37+
{
38+
public float x, y, z;
39+
public Vector3Data(Vector3 v) { x = v.x; y = v.y; z = v.z; }
40+
}
41+
42+
public static List<HierarchyNode> GetSceneHierarchyData(Scene scene, int maxDepth = 20)
1443
{
1544
if (!scene.IsValid() || !scene.isLoaded)
1645
{
17-
return new List<object>();
46+
return new List<HierarchyNode>();
1847
}
1948

2049
GameObject[] rootObjects = scene.GetRootGameObjects();
21-
return rootObjects.Select(go => GetGameObjectDataRecursive(go)).ToList();
50+
// Use initial capacity to avoid resizing
51+
var result = new List<HierarchyNode>(rootObjects.Length);
52+
53+
foreach (var go in rootObjects)
54+
{
55+
result.Add(GetGameObjectDataRecursive(go, 0, maxDepth));
56+
}
57+
return result;
2258
}
2359

2460
/// <summary>
2561
/// Recursively builds a data representation of a GameObject and its children.
62+
/// Uses DTOs to minimize GC overhead.
2663
/// </summary>
27-
public static object GetGameObjectDataRecursive(GameObject go)
64+
public static HierarchyNode GetGameObjectDataRecursive(GameObject go, int currentDepth, int maxDepth)
2865
{
29-
if (go == null)
30-
return null;
31-
32-
var childrenData = new List<object>();
33-
foreach (Transform child in go.transform)
34-
{
35-
childrenData.Add(GetGameObjectDataRecursive(child.gameObject));
36-
}
66+
if (go == null) return null;
3767

38-
var gameObjectData = new Dictionary<string, object>
68+
var node = new HierarchyNode
3969
{
40-
{ "name", go.name },
41-
{ "activeSelf", go.activeSelf },
42-
{ "activeInHierarchy", go.activeInHierarchy },
43-
{ "tag", go.tag },
44-
{ "layer", go.layer },
45-
{ "isStatic", go.isStatic },
46-
{ "instanceID", go.GetInstanceID() },
70+
name = go.name,
71+
activeSelf = go.activeSelf,
72+
activeInHierarchy = go.activeInHierarchy,
73+
tag = go.tag,
74+
layer = go.layer,
75+
isStatic = go.isStatic,
76+
instanceID = go.GetInstanceID(),
77+
transform = new TransformData
4778
{
48-
"transform",
49-
new
50-
{
51-
position = new
52-
{
53-
x = go.transform.localPosition.x,
54-
y = go.transform.localPosition.y,
55-
z = go.transform.localPosition.z,
56-
},
57-
rotation = new
58-
{
59-
x = go.transform.localRotation.eulerAngles.x,
60-
y = go.transform.localRotation.eulerAngles.y,
61-
z = go.transform.localRotation.eulerAngles.z,
62-
},
63-
scale = new
64-
{
65-
x = go.transform.localScale.x,
66-
y = go.transform.localScale.y,
67-
z = go.transform.localScale.z,
68-
},
69-
}
70-
},
71-
{ "children", childrenData },
79+
position = new Vector3Data(go.transform.localPosition),
80+
rotation = new Vector3Data(go.transform.localRotation.eulerAngles),
81+
scale = new Vector3Data(go.transform.localScale)
82+
}
7283
};
7384

74-
return gameObjectData;
85+
if (currentDepth < maxDepth && go.transform.childCount > 0)
86+
{
87+
node.children = new List<HierarchyNode>(go.transform.childCount);
88+
foreach (Transform child in go.transform)
89+
{
90+
node.children.Add(GetGameObjectDataRecursive(child.gameObject, currentDepth + 1, maxDepth));
91+
}
92+
}
93+
94+
return node;
7595
}
7696
}
7797
}

MCPForUnity/Editor/Tools/ManageScene.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ private sealed class SceneCommand
2727
public int? buildIndex { get; set; }
2828
public string fileName { get; set; } = string.Empty;
2929
public int? superSize { get; set; }
30+
public int? maxDepth { get; set; }
3031
}
3132

3233
private static SceneCommand ToSceneCommand(JObject p)
@@ -48,7 +49,8 @@ private static SceneCommand ToSceneCommand(JObject p)
4849
path = p["path"]?.ToString() ?? string.Empty,
4950
buildIndex = BI(p["buildIndex"] ?? p["build_index"]),
5051
fileName = (p["fileName"] ?? p["filename"])?.ToString() ?? string.Empty,
51-
superSize = BI(p["superSize"] ?? p["super_size"] ?? p["supersize"])
52+
superSize = BI(p["superSize"] ?? p["super_size"] ?? p["supersize"]),
53+
maxDepth = BI(p["maxDepth"] ?? p["max_depth"])
5254
};
5355
}
5456

@@ -138,7 +140,7 @@ public static object HandleCommand(JObject @params)
138140
return SaveScene(fullPath, relativePath);
139141
case "get_hierarchy":
140142
try { McpLog.Info("[ManageScene] get_hierarchy: entering", always: false); } catch { }
141-
var gh = GetSceneHierarchy();
143+
var gh = GetSceneHierarchy(cmd.maxDepth);
142144
try { McpLog.Info("[ManageScene] get_hierarchy: exiting", always: false); } catch { }
143145
return gh;
144146
case "get_active":
@@ -453,7 +455,7 @@ private static object GetBuildSettingsScenes()
453455
}
454456
}
455457

456-
private static object GetSceneHierarchy()
458+
private static object GetSceneHierarchy(int? maxDepth)
457459
{
458460
try
459461
{
@@ -468,7 +470,7 @@ private static object GetSceneHierarchy()
468470
}
469471

470472
try { McpLog.Info("[ManageScene] get_hierarchy: fetching hierarchy data", always: false); } catch { }
471-
var hierarchy = SceneTraversalService.GetSceneHierarchyData(activeScene);
473+
var hierarchy = SceneTraversalService.GetSceneHierarchyData(activeScene, maxDepth ?? 20);
472474

473475
var resp = new SuccessResponse(
474476
$"Retrieved hierarchy for scene '{activeScene.name}'.",

0 commit comments

Comments
 (0)