Optimise so startup is fast again (#494)

* Optimize tool loading so startup is fast again

We lazy load tools, remove the expensive AssetPath property, and reflect for only `McpForUnityToolAttribute`, so it's much faster.

A 6 second startup is now back to 400ms. Can still be optimised but this is good

* Remove .meta file from tests

The tests automatically cleans this up, so it likely got pushed by accident
main
Marcus Sanatan 2025-12-29 18:39:03 -04:00 committed by GitHub
parent 6f080f5390
commit 1a7f4bb4a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 54 additions and 166 deletions

View File

@ -14,7 +14,6 @@ namespace MCPForUnity.Editor.Services
public string ClassName { get; set; } public string ClassName { get; set; }
public string Namespace { get; set; } public string Namespace { get; set; }
public string AssemblyName { get; set; } public string AssemblyName { get; set; }
public string AssetPath { get; set; }
public bool AutoRegister { get; set; } = true; public bool AutoRegister { get; set; } = true;
public bool RequiresPolling { get; set; } = false; public bool RequiresPolling { get; set; } = false;
public string PollAction { get; set; } = "status"; public string PollAction { get; set; } = "status";

View File

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text.RegularExpressions;
using MCPForUnity.Editor.Constants; using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Tools; using MCPForUnity.Editor.Tools;
@ -13,8 +12,7 @@ namespace MCPForUnity.Editor.Services
public class ToolDiscoveryService : IToolDiscoveryService public class ToolDiscoveryService : IToolDiscoveryService
{ {
private Dictionary<string, ToolMetadata> _cachedTools; private Dictionary<string, ToolMetadata> _cachedTools;
private readonly Dictionary<Type, string> _scriptPathCache = new();
private readonly Dictionary<string, string> _summaryCache = new();
public List<ToolMetadata> DiscoverAllTools() public List<ToolMetadata> DiscoverAllTools()
{ {
@ -25,20 +23,24 @@ namespace MCPForUnity.Editor.Services
_cachedTools = new Dictionary<string, ToolMetadata>(); _cachedTools = new Dictionary<string, ToolMetadata>();
// Scan all assemblies for [McpForUnityTool] attributes var toolTypes = TypeCache.GetTypesWithAttribute<McpForUnityToolAttribute>();
var assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (var type in toolTypes)
foreach (var assembly in assemblies)
{ {
McpForUnityToolAttribute toolAttr;
try try
{ {
var types = assembly.GetTypes(); toolAttr = type.GetCustomAttribute<McpForUnityToolAttribute>();
}
foreach (var type in types) catch (Exception ex)
{ {
var toolAttr = type.GetCustomAttribute<McpForUnityToolAttribute>(); McpLog.Warn($"Failed to read [McpForUnityTool] for {type.FullName}: {ex.Message}");
if (toolAttr == null)
continue; continue;
}
if (toolAttr == null)
{
continue;
}
var metadata = ExtractToolMetadata(type, toolAttr); var metadata = ExtractToolMetadata(type, toolAttr);
if (metadata != null) if (metadata != null)
@ -47,13 +49,6 @@ namespace MCPForUnity.Editor.Services
EnsurePreferenceInitialized(metadata); EnsurePreferenceInitialized(metadata);
} }
} }
}
catch (Exception ex)
{
// Skip assemblies that can't be reflected
McpLog.Info($"Skipping assembly {assembly.FullName}: {ex.Message}");
}
}
McpLog.Info($"Discovered {_cachedTools.Count} MCP tools via reflection"); McpLog.Info($"Discovered {_cachedTools.Count} MCP tools via reflection");
return _cachedTools.Values.ToList(); return _cachedTools.Values.ToList();
@ -131,21 +126,13 @@ namespace MCPForUnity.Editor.Services
ClassName = type.Name, ClassName = type.Name,
Namespace = type.Namespace ?? "", Namespace = type.Namespace ?? "",
AssemblyName = type.Assembly.GetName().Name, AssemblyName = type.Assembly.GetName().Name,
AssetPath = ResolveScriptAssetPath(type),
AutoRegister = toolAttr.AutoRegister, AutoRegister = toolAttr.AutoRegister,
RequiresPolling = toolAttr.RequiresPolling, RequiresPolling = toolAttr.RequiresPolling,
PollAction = string.IsNullOrEmpty(toolAttr.PollAction) ? "status" : toolAttr.PollAction PollAction = string.IsNullOrEmpty(toolAttr.PollAction) ? "status" : toolAttr.PollAction
}; };
metadata.IsBuiltIn = DetermineIsBuiltIn(type, metadata); metadata.IsBuiltIn = DetermineIsBuiltIn(type, metadata);
if (metadata.IsBuiltIn)
{
string summaryDescription = ExtractSummaryDescription(type, metadata);
if (!string.IsNullOrWhiteSpace(summaryDescription))
{
metadata.Description = summaryDescription;
}
}
return metadata; return metadata;
} }
@ -265,56 +252,6 @@ namespace MCPForUnity.Editor.Services
return EditorPrefKeys.ToolEnabledPrefix + toolName; return EditorPrefKeys.ToolEnabledPrefix + toolName;
} }
private string ResolveScriptAssetPath(Type type)
{
if (type == null)
{
return null;
}
if (_scriptPathCache.TryGetValue(type, out var cachedPath))
{
return cachedPath;
}
string resolvedPath = null;
try
{
string filter = string.IsNullOrEmpty(type.Name) ? "t:MonoScript" : $"{type.Name} t:MonoScript";
var guids = AssetDatabase.FindAssets(filter);
foreach (var guid in guids)
{
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
if (string.IsNullOrEmpty(assetPath))
{
continue;
}
var script = AssetDatabase.LoadAssetAtPath<MonoScript>(assetPath);
if (script == null)
{
continue;
}
var scriptClass = script.GetClass();
if (scriptClass == type)
{
resolvedPath = assetPath.Replace('\\', '/');
break;
}
}
}
catch (Exception ex)
{
McpLog.Warn($"Failed to resolve asset path for {type.FullName}: {ex.Message}");
}
_scriptPathCache[type] = resolvedPath;
return resolvedPath;
}
private bool DetermineIsBuiltIn(Type type, ToolMetadata metadata) private bool DetermineIsBuiltIn(Type type, ToolMetadata metadata)
{ {
if (metadata == null) if (metadata == null)
@ -322,26 +259,10 @@ namespace MCPForUnity.Editor.Services
return false; return false;
} }
if (!string.IsNullOrEmpty(metadata.AssetPath)) if (type != null && !string.IsNullOrEmpty(type.Namespace) && type.Namespace.StartsWith("MCPForUnity.Editor.Tools", StringComparison.Ordinal))
{
string normalizedPath = metadata.AssetPath.Replace("\\", "/");
string packageRoot = AssetPathUtility.GetMcpPackageRootPath();
if (!string.IsNullOrEmpty(packageRoot))
{
string normalizedRoot = packageRoot.Replace("\\", "/");
if (!normalizedRoot.EndsWith("/", StringComparison.Ordinal))
{
normalizedRoot += "/";
}
string builtInRoot = normalizedRoot + "Editor/Tools/";
if (normalizedPath.StartsWith(builtInRoot, StringComparison.OrdinalIgnoreCase))
{ {
return true; return true;
} }
}
}
if (!string.IsNullOrEmpty(metadata.AssemblyName) && metadata.AssemblyName.Equals("MCPForUnity.Editor", StringComparison.Ordinal)) if (!string.IsNullOrEmpty(metadata.AssemblyName) && metadata.AssemblyName.Equals("MCPForUnity.Editor", StringComparison.Ordinal))
{ {
@ -350,57 +271,5 @@ namespace MCPForUnity.Editor.Services
return false; return false;
} }
private string ExtractSummaryDescription(Type type, ToolMetadata metadata)
{
if (metadata == null || string.IsNullOrEmpty(metadata.AssetPath))
{
return null;
}
if (_summaryCache.TryGetValue(metadata.AssetPath, out var cachedSummary))
{
return cachedSummary;
}
string summary = null;
try
{
var monoScript = AssetDatabase.LoadAssetAtPath<MonoScript>(metadata.AssetPath);
string scriptText = monoScript?.text;
if (string.IsNullOrEmpty(scriptText))
{
_summaryCache[metadata.AssetPath] = null;
return null;
}
string classPattern = $@"///\s*<summary>\s*(?<content>[\s\S]*?)\s*</summary>\s*(?:\[[^\]]*\]\s*)*(?:public\s+)?(?:static\s+)?class\s+{Regex.Escape(type.Name)}";
var match = Regex.Match(scriptText, classPattern);
if (!match.Success)
{
match = Regex.Match(scriptText, @"///\s*<summary>\s*(?<content>[\s\S]*?)\s*</summary>");
}
if (!match.Success)
{
_summaryCache[metadata.AssetPath] = null;
return null;
}
summary = match.Groups["content"].Value;
summary = Regex.Replace(summary, @"^\s*///\s?", string.Empty, RegexOptions.Multiline);
summary = Regex.Replace(summary, @"<[^>]+>", string.Empty);
summary = Regex.Replace(summary, @"\s+", " ").Trim();
}
catch (System.Exception ex)
{
McpLog.Warn($"Failed to extract summary description for {type?.FullName}: {ex.Message}");
}
_summaryCache[metadata.AssetPath] = summary;
return summary;
}
} }
} }

View File

@ -2,17 +2,17 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Services; using MCPForUnity.Editor.Services;
using MCPForUnity.Editor.Windows.Components.ClientConfig; using MCPForUnity.Editor.Windows.Components.ClientConfig;
using MCPForUnity.Editor.Windows.Components.Connection; using MCPForUnity.Editor.Windows.Components.Connection;
using MCPForUnity.Editor.Windows.Components.Settings; using MCPForUnity.Editor.Windows.Components.Settings;
using MCPForUnity.Editor.Windows.Components.Tools;
using UnityEditor; using UnityEditor;
using UnityEditor.UIElements; using UnityEditor.UIElements;
using UnityEngine; using UnityEngine;
using UnityEngine.UIElements; using UnityEngine.UIElements;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Windows.Components.Tools;
namespace MCPForUnity.Editor.Windows namespace MCPForUnity.Editor.Windows
{ {
@ -31,6 +31,7 @@ namespace MCPForUnity.Editor.Windows
private static readonly HashSet<MCPForUnityEditorWindow> OpenWindows = new(); private static readonly HashSet<MCPForUnityEditorWindow> OpenWindows = new();
private bool guiCreated = false; private bool guiCreated = false;
private bool toolsLoaded = false;
private double lastRefreshTime = 0; private double lastRefreshTime = 0;
private const double RefreshDebounceSeconds = 0.5; private const double RefreshDebounceSeconds = 0.5;
@ -196,18 +197,39 @@ namespace MCPForUnity.Editor.Windows
var toolsRoot = toolsTree.Instantiate(); var toolsRoot = toolsTree.Instantiate();
toolsContainer.Add(toolsRoot); toolsContainer.Add(toolsRoot);
toolsSection = new McpToolsSection(toolsRoot); toolsSection = new McpToolsSection(toolsRoot);
toolsSection.Refresh();
if (toolsTabToggle != null && toolsTabToggle.value)
{
EnsureToolsLoaded();
}
} }
else else
{ {
McpLog.Warn("Failed to load tools section UXML. Tool configuration will be unavailable."); McpLog.Warn("Failed to load tools section UXML. Tool configuration will be unavailable.");
} }
guiCreated = true; guiCreated = true;
// Initial updates // Initial updates
RefreshAllData(); RefreshAllData();
} }
private void EnsureToolsLoaded()
{
if (toolsLoaded)
{
return;
}
if (toolsSection == null)
{
return;
}
toolsLoaded = true;
toolsSection.Refresh();
}
private void OnEnable() private void OnEnable()
{ {
EditorApplication.update += OnEditorUpdate; EditorApplication.update += OnEditorUpdate;
@ -219,6 +241,7 @@ namespace MCPForUnity.Editor.Windows
EditorApplication.update -= OnEditorUpdate; EditorApplication.update -= OnEditorUpdate;
OpenWindows.Remove(this); OpenWindows.Remove(this);
guiCreated = false; guiCreated = false;
toolsLoaded = false;
} }
private void OnFocus() private void OnFocus()
@ -327,6 +350,11 @@ namespace MCPForUnity.Editor.Windows
settingsTabToggle?.SetValueWithoutNotify(showSettings); settingsTabToggle?.SetValueWithoutNotify(showSettings);
toolsTabToggle?.SetValueWithoutNotify(!showSettings); toolsTabToggle?.SetValueWithoutNotify(!showSettings);
if (!showSettings)
{
EnsureToolsLoaded();
}
EditorPrefs.SetString(EditorPrefKeys.EditorWindowActivePanel, panel.ToString()); EditorPrefs.SetString(EditorPrefKeys.EditorWindowActivePanel, panel.ToString());
} }

View File

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 20332651bb6f64cadb92cf3c6d68bed5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: