feat: integrate performance upgrade and agent inspection tooling
Validate ShrinkSDK Workspace / catalog (push) Successful in 5s
Validate ShrinkSDK Workspace / unity (push) Successful in 3m20s

This commit is contained in:
2026-09-29 10:16:02 +08:00
parent f92105c97d
commit 08246ddb01
114 changed files with 7135 additions and 87 deletions
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup><TargetFramework>netstandard2.1</TargetFramework><LangVersion>latest</LangVersion><Nullable>enable</Nullable><EnableDefaultCompileItems>false</EnableDefaultCompileItems><PackageId>ShrinkSDK.Inspection</PackageId><Version>0.1.0</Version><AssemblyName>ShrinkInspection.Runtime</AssemblyName></PropertyGroup>
<ItemGroup>
<Compile Include="../Runtime/**/*.cs" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="ShrinkSDK.Context.Core" Version="0.3.0" />
<PackageReference Include="ShrinkSDK.EventBus" Version="2.2.2" />
<PackageReference Include="ShrinkSDK.Network" Version="0.4.2" />
<PackageReference Include="ShrinkSDK.Command" Version="0.4.2" />
</ItemGroup>
</Project>
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2274a33f3eae9774db82c4e478f2f90d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,6 @@
{
"name": "ShrinkInspection.Editor",
"references": ["ShrinkInspection.Runtime", "ShrinkContext.Core.Runtime", "ShrinkEventBus.Runtime", "Newtonsoft.Json"],
"includePlatforms": ["Editor"],
"autoReferenced": true
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 8a20e065e3e35a94e9e3c1c29328219f
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,61 @@
#nullable enable
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using Newtonsoft.Json;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEngine;
using ShrinkContext;
using ShrinkEventBus;
namespace ShrinkSDK.Inspection.Editor
{
public static class ShrinkInspectionExport
{
private const string DirectoryName = "Artifacts/Inspection";
/// <summary>Host extensions add explicitly owned Network/Command/Context instances. Remove handlers when the host shuts down.</summary>
public static event Action<ShrinkInspectionSnapshot>? CaptureHostState;
[MenuItem("Tools/ShrinkSDK/Inspection/Export compilation inputs")]
public static void ExportCompilation()
{
RequireStableEditor();
var root = Path.GetFullPath(".");
var assemblies = CompilationPipeline.GetAssemblies(AssembliesType.Player);
var loaded = AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic).Select(a => a.GetName().Name).ToHashSet();
var identityNames = assemblies.Select(a => a.name).Where(loaded.Contains).OrderBy(n => n, StringComparer.Ordinal).ToArray();
var inputs = assemblies.Select(a => new { Name = a.name, Sources = a.sourceFiles.Select(Path.GetFullPath).ToArray(),
References = a.allReferences.Select(Path.GetFullPath).Where(File.Exists).ToArray(), Defines = a.defines, Output = Path.GetFullPath(a.outputPath) }).ToArray();
var ticks = inputs.SelectMany(a => a.Sources.Concat(a.References)).Distinct(StringComparer.OrdinalIgnoreCase)
.ToDictionary(path => path, path => File.GetLastWriteTimeUtc(path).Ticks);
Directory.CreateDirectory(DirectoryName);
File.WriteAllText(DirectoryName + "/compilation.json", JsonConvert.SerializeObject(new { Root = root,
BuildId = ShrinkInspectionSnapshot.BuildIdentity(identityNames), IdentityAssemblies = identityNames, Assemblies = inputs, SourceTicks = ticks }, Formatting.Indented));
Debug.Log("Shrink inspection compilation exported: " + Path.GetFullPath(DirectoryName + "/compilation.json"));
}
[MenuItem("Tools/ShrinkSDK/Inspection/Export runtime snapshot")]
public static void ExportRuntime()
{
RequireStableEditor();
var path = DirectoryName + "/compilation.json";
if (!File.Exists(path)) throw new InvalidOperationException("Export compilation inputs first.");
var manifest = JsonConvert.DeserializeObject<IdentityManifest>(File.ReadAllText(path))!;
var snapshot = new ShrinkInspectionSnapshot(ShrinkInspectionSnapshot.BuildIdentity(manifest.IdentityAssemblies));
snapshot.AddContext("default-context", ShrinkContextDefaults.Default);
var ordinal = 0;
foreach (var bus in EventBus.CaptureNamedBuses()) snapshot.AddEventBus("named-bus-" + ordinal++, bus);
snapshot.Coverage.Add("Default Context and named EventBus instances only; host-owned instances require CaptureHostState registration.");
CaptureHostState?.Invoke(snapshot);
File.WriteAllText(DirectoryName + "/runtime.json", snapshot.ToJson());
Debug.Log("Shrink inspection runtime exported: " + Path.GetFullPath(DirectoryName + "/runtime.json"));
}
private static void RequireStableEditor()
{
if (EditorApplication.isCompiling || EditorApplication.isUpdating) throw new InvalidOperationException("Wait for Editor compilation/import to finish.");
}
private sealed class IdentityManifest { public string[] IdentityAssemblies = Array.Empty<string>(); }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 35c6f0febfce34543a9efb401ce8b3b6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+23
View File
@@ -0,0 +1,23 @@
# Shrink Inspection
可选诊断接入包,提供 Unity 编译输入导出和跨 Unity/Godot/.NET 的按需绑定快照。它依赖被观察模块,不进入它们的热路径。
Unity 安装本目录的 UPM 包后,使用 `Tools/ShrinkSDK/Inspection` 导出编译输入和运行快照。结果位于项目的 `Artifacts/Inspection`。在命令行使用:
```text
shrink inspect flow command:execute --compilation Artifacts/Inspection/compilation.json --snapshot Artifacts/Inspection/runtime.json
```
编辑器默认只捕获默认 Context 与命名 EventBus。宿主持有的独立实例通过 `ShrinkInspectionExport.CaptureHostState` 按需加入;宿主退出时取消该委托,避免诊断代码持有已退役对象。`AddContext` 可同时传入 Loader,记录最近事务的阶段和恢复结果。
Godot/.NET 使用 `ShrinkSDK.Inspection` 和相同的 `ShrinkInspectionSnapshot` API。宿主在所属线程创建快照,调用 `AddContext`、`AddNetwork`、`AddEventBus`、`AddCommands`,再写入 `ToJson()`。构建标识由明确的程序集名集合计算;必须与编译输入的程序集集合和 MVID 一致。未知构建不能通过手写相同字符串伪装为已验证关联。
捕获时暂停同一实例的注册、退役和组合修改。快照只含符号、状态和计数;不含消息正文、会话令牌、存档内容及异常文本。它记录快照时刻的绑定,不提供完整调用时序。不同 scope 的绑定不能拼成已经确认的端到端调用。
<!-- shrink:capabilities -->
Capability: 导出真实编译输入、Context/provider 状态、EventBus/Network/Command 当前绑定及队列指标
Aliases: 导航 调试 诊断 快照 inspect snapshot 调用链
Limits: 按需快照,不是调用时序追踪;只覆盖显式加入的实例;过期或不同构建不能合并
Extension: 宿主在所属线程调用 AddContext/AddNetwork/AddEventBus/AddCommands,Unity 使用 CaptureHostState
Evidence: [快照 API](Runtime/ShrinkInspectionSnapshot.cs); [Unity 导出](Editor/ShrinkInspectionExport.cs)
<!-- /shrink:capabilities -->
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: f566b821eafe5fa498c7504ccf0d40aa
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 87749fd61a2f5e44aab3896a7a873b38
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,6 @@
{
"name": "ShrinkInspection.Runtime",
"references": ["ShrinkContext.Core.Runtime", "ShrinkEventBus.Runtime", "ShrinkNetwork.Runtime", "ShrinkCommand.Runtime", "Newtonsoft.Json"],
"autoReferenced": true,
"noEngineReferences": true
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 4210356b49bcdf240ad3c08659b7f720
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,102 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json;
using ShrinkContext;
using ShrinkEventBus;
using ShrinkNetwork;
using ShrinkCommand;
namespace ShrinkSDK.Inspection
{
public sealed class ShrinkInspectionRelation
{
public string From = "";
public string To = "";
public string Kind = "";
public string Scope = "";
}
/// <summary>Explicit, on-demand snapshot. Call from the owning host thread while registration/composition is stable.</summary>
public sealed class ShrinkInspectionSnapshot
{
public int FormatVersion = 1;
public string BuildId = "";
public string CapturedAtUtc = DateTime.UtcNow.ToString("O");
public readonly List<string> Coverage = new();
public readonly List<ShrinkInspectionRelation> Relations = new();
public readonly Dictionary<string, object> State = new();
public static string BuildIdentity(IEnumerable<string> assemblyNames)
{
var loaded = AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic).ToArray();
var identity = string.Join(";", assemblyNames.Distinct().OrderBy(n => n, StringComparer.Ordinal).Select(name =>
{
var candidates = loaded.Where(a => a.GetName().Name == name).ToArray();
if (candidates.Length != 1) throw new InvalidOperationException("Cannot establish unique runtime assembly identity: " + name);
return name + ":" + candidates[0].ManifestModule.ModuleVersionId.ToString("N");
}));
using var sha = System.Security.Cryptography.SHA256.Create();
return BitConverter.ToString(sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(identity))).Replace("-", "").ToLowerInvariant();
}
public ShrinkInspectionSnapshot(string buildId) { BuildId = buildId; }
public string ToJson() => JsonConvert.SerializeObject(this, Formatting.Indented, new Newtonsoft.Json.Converters.StringEnumConverter());
public void AddContext(string scope, ShrinkContextRuntime runtime, ShrinkContextLoader? loader = null)
{
var snapshot = runtime.CaptureDiagnostic();
State.Add(scope, new { snapshot.BindingCount, snapshot.Notifications,
Fibers = snapshot.Fibers.Select(f => new { f.Uid, f.Name, f.Status, f.Retired, f.InTransition, f.Inject, f.Provide, f.Dependencies, f.ErrorType }).ToArray(),
Transaction = loader?.LastTransaction is { } t ? new { t.Generation, t.Phase, t.CurrentEntryId, t.ErrorType, t.RestoreAttempted, t.PreviousCompositionRestored, t.RestoreErrorType } : null });
foreach (var fiber in snapshot.Fibers.Where(f => !f.Retired))
foreach (var dependency in fiber.Dependencies)
{
Add("fiber:" + scope + ":" + fiber.Uid, "key:" + dependency.Key, "requires@" + dependency.Realm, scope);
if (dependency.CurrentProviderUid.HasValue)
Add("fiber:" + scope + ":" + dependency.CurrentProviderUid, "fiber:" + scope + ":" + fiber.Uid, "provider:" + dependency.Key + "@" + dependency.Realm, scope);
}
}
public void AddNetwork(string scope, ShrinkNetworkService service)
{
State.Add(scope, new { Metrics = service.GetDiagnosticsSnapshot(), Queue = service.DispatchScheduler is ShrinkNetworkDispatchQueue queued ? queued.CaptureDiagnostics() : (service.DispatchScheduler as ShrinkNetworkInlineDispatchScheduler)?.CaptureDiagnostics() });
foreach (var meta in service.MessageRegistry.Registrations)
{
Add("opcode:" + meta.Opcode, "type:" + TypeName(meta.MessageType), "message", scope);
if (!string.IsNullOrEmpty(meta.Route)) Add("route:" + meta.Route, "type:" + TypeName(meta.MessageType), "message", scope);
}
foreach (var pair in service.Router.CaptureBindings()) Add("type:" + TypeName(pair.Key), MethodId(pair.Value), "handler", scope);
}
public void AddEventBus(string scope, IShrinkEventBus bus)
{
try
{
var bindings = EventBus.CaptureBindings(bus);
State.Add(scope, new { Bus = bus.Key.ToString(), Subscribers = bindings.Count });
foreach (var binding in bindings) Add("type:" + TypeName(binding.EventType), MethodId(binding.Handler), "subscribes", scope);
}
catch (NotSupportedException) { Coverage.Add(scope + ": custom EventBus does not expose bindings."); }
}
public void AddCommands(string scope, ShrinkCommandService service)
{
State.Add(scope, new { Registrations = service.Commands.Select(c => new { c.Path, c.Aliases, c.Permission, c.SourceKind }).ToArray() });
foreach (var binding in service.CaptureBindings()) Add("command:" + binding.Path, MethodId(binding.Handler), "command", scope);
}
private void Add(string from, string to, string kind, string scope) => Relations.Add(new ShrinkInspectionRelation { From = from, To = to, Kind = kind, Scope = scope });
private static string TypeName(Type type) => (type.FullName ?? type.Name).Replace('+', '.');
public static string MethodId(MethodInfo method)
{
// Generated wrappers and generic signatures remain runtime-only endpoints rather than guessed source matches.
if (method.IsGenericMethod || method.DeclaringType?.IsGenericType == true || method.GetParameters().Any(p => p.ParameterType.IsGenericType || p.ParameterType.IsArray || p.ParameterType.IsByRef))
return "runtime-method:" + method.Module.ModuleVersionId.ToString("N") + ":" + method.MetadataToken;
var parameters = method.GetParameters();
return "M:" + TypeName(method.DeclaringType!) + "." + method.Name + (parameters.Length == 0 ? "" : "(" + string.Join(",", parameters.Select(p => TypeName(p.ParameterType))) + ")");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a412b2f6a531a33419d270e47853ef05
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+13
View File
@@ -0,0 +1,13 @@
{
"name": "com.cneicy.shrink-inspection",
"version": "0.1.0",
"displayName": "Shrink Inspection",
"unity": "2022.3",
"description": "On-demand build inputs and bounded runtime binding snapshots for shrink inspect.",
"dependencies": {
"com.cneicy.shrink-context-core": "0.3.0",
"com.cneicy.shrink-eventbus": "2.2.2",
"com.cneicy.shrink-network": "0.4.2",
"com.cneicy.shrink-command": "0.4.2"
}
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: fdb80de9a8588bc4392a94dd7352be58
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: