Add Unity Pipeline package support
This commit is contained in:
+89
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Unity.Pipeline.Commands;
|
||||
|
||||
namespace Unity.Pipeline.Editor.Commands.Observability
|
||||
{
|
||||
/// <summary>
|
||||
/// Read-only observability commands over the captured Editor console (CLI-198). Logs are captured
|
||||
/// continuously by <see cref="ConsoleLogBuffer"/>; these commands let an agent read them back as
|
||||
/// structured data and clear the buffer (and the Unity console) between runs.
|
||||
/// </summary>
|
||||
public static class ConsoleCommands
|
||||
{
|
||||
[CliCommand("get_console_logs", "Read recently captured Editor console logs (structured).")]
|
||||
public static object GetConsoleLogs(
|
||||
[CliArg("severity", "Filter: all | log | warning | error. 'all' = every entry; 'log' = Log only; 'warning' = Warning only; 'error' = Error/Exception/Assert only.")] string severity = "all",
|
||||
[CliArg("limit", "Max entries to return (most-recent first), capped at 1000.")] int limit = 100)
|
||||
{
|
||||
// Snapshot is oldest-first; filter by tier, then take the most-recent `limit` entries
|
||||
// and present them newest-first.
|
||||
var snapshot = ConsoleLogBuffer.Snapshot();
|
||||
var filtered = new List<ConsoleLogEntryDto>(snapshot.Count);
|
||||
foreach (var entry in snapshot)
|
||||
{
|
||||
if (MatchesSeverity(entry.Type, severity))
|
||||
filtered.Add(entry);
|
||||
}
|
||||
|
||||
var clampedLimit = Math.Min(Math.Max(limit, 1), ConsoleLogBuffer.MaxEntries);
|
||||
|
||||
var logs = new List<ConsoleLogEntryDto>(Math.Min(clampedLimit, filtered.Count));
|
||||
for (var i = filtered.Count - 1; i >= 0 && logs.Count < clampedLimit; i--)
|
||||
logs.Add(filtered[i]);
|
||||
|
||||
return new
|
||||
{
|
||||
total = filtered.Count,
|
||||
returned = logs.Count,
|
||||
logs
|
||||
};
|
||||
}
|
||||
|
||||
[CliCommand("clear_console", "Clear the captured log buffer and the Unity Editor console.")]
|
||||
public static object ClearConsole()
|
||||
{
|
||||
ConsoleLogBuffer.Clear();
|
||||
|
||||
// Best-effort: also clear Unity's own console window. UnityEditor.LogEntries is internal,
|
||||
// so reach it via reflection and swallow any failure (API is not part of the public contract).
|
||||
// LogEntries.Clear is a static, non-public method, so include NonPublic in the binding flags —
|
||||
// GetMethod("Clear") with no flags only finds public methods and would silently return null.
|
||||
try
|
||||
{
|
||||
Type.GetType("UnityEditor.LogEntries,UnityEditor")
|
||||
?.GetMethod("Clear", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)
|
||||
?.Invoke(null, null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore — clearing the Editor console is auxiliary; the buffer is already cleared.
|
||||
}
|
||||
|
||||
return new { cleared = true };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Map a Unity <see cref="UnityEngine.LogType"/> name onto the requested severity tier.
|
||||
/// The tiers are distinct: "all" matches every entry; "log" matches Log only;
|
||||
/// "warning" matches Warning only; "error" matches Error/Exception/Assert only.
|
||||
/// Any unknown value falls back to "all" (matches everything).
|
||||
/// </summary>
|
||||
private static bool MatchesSeverity(string type, string severity)
|
||||
{
|
||||
switch (severity?.ToLowerInvariant())
|
||||
{
|
||||
case "log":
|
||||
return type == "Log";
|
||||
case "warning":
|
||||
return type == "Warning";
|
||||
case "error":
|
||||
return type == "Error" || type == "Exception" || type == "Assert";
|
||||
case "all":
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b7dd929510db8d248b64b05361bfabb6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Pipeline.Editor.Commands.Observability
|
||||
{
|
||||
/// <summary>
|
||||
/// A single captured Editor console entry. Returned (most-recent-first) by the
|
||||
/// <c>get_console_logs</c> command so an agent can read structured log output without scraping
|
||||
/// the Editor UI. Mirrors the fields Unity surfaces on <see cref="Application.logMessageReceivedThreaded"/>.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class ConsoleLogEntryDto
|
||||
{
|
||||
/// <summary>Unity <see cref="LogType"/> name, e.g. "Log", "Warning", "Error", "Exception", "Assert".</summary>
|
||||
[JsonProperty("type")]
|
||||
public string Type { get; set; }
|
||||
|
||||
/// <summary>The logged message text.</summary>
|
||||
[JsonProperty("message")]
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>Captured stack trace, if Unity provided one for the entry.</summary>
|
||||
[JsonProperty("stackTrace")]
|
||||
public string StackTrace { get; set; }
|
||||
|
||||
/// <summary>Capture time in UTC, ISO-8601 round-trip ("o") format.</summary>
|
||||
[JsonProperty("timestampUtc")]
|
||||
public string TimestampUtc { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures Editor console output into a bounded, thread-safe ring buffer so it can be read back
|
||||
/// structurally over the pipeline (CLI-198). Subscribes to
|
||||
/// <see cref="Application.logMessageReceivedThreaded"/> on load — the threaded variant is used
|
||||
/// because Unity may raise log callbacks off the main thread, and the handler is fully lock-guarded.
|
||||
///
|
||||
/// The buffer holds at most <see cref="MaxEntries"/> entries; the oldest is dropped when full.
|
||||
/// </summary>
|
||||
public static class ConsoleLogBuffer
|
||||
{
|
||||
/// <summary>Maximum number of entries retained; oldest entries are dropped past this.</summary>
|
||||
public const int MaxEntries = 1000;
|
||||
|
||||
private static readonly object m_Lock = new object();
|
||||
private static readonly Queue<ConsoleLogEntryDto> m_Entries = new Queue<ConsoleLogEntryDto>(MaxEntries);
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to Unity's threaded log callback as soon as the Editor domain loads, so capture
|
||||
/// is live before any command runs. Idempotent across domain reloads (handler is removed
|
||||
/// first to avoid double-subscription if Init somehow runs twice).
|
||||
/// </summary>
|
||||
[InitializeOnLoadMethod]
|
||||
private static void Init()
|
||||
{
|
||||
Application.logMessageReceivedThreaded -= OnLogMessageThreaded;
|
||||
Application.logMessageReceivedThreaded += OnLogMessageThreaded;
|
||||
}
|
||||
|
||||
private static void OnLogMessageThreaded(string condition, string stackTrace, LogType type)
|
||||
{
|
||||
var entry = new ConsoleLogEntryDto
|
||||
{
|
||||
Type = type.ToString(),
|
||||
Message = condition,
|
||||
StackTrace = stackTrace,
|
||||
TimestampUtc = DateTime.UtcNow.ToString("o")
|
||||
};
|
||||
|
||||
lock (m_Lock)
|
||||
{
|
||||
if (m_Entries.Count >= MaxEntries)
|
||||
m_Entries.Dequeue();
|
||||
m_Entries.Enqueue(entry);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Take a point-in-time copy of the buffer in chronological (oldest-first) order. The copy is
|
||||
/// detached from the live buffer, so callers can filter/reverse it without holding the lock.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<ConsoleLogEntryDto> Snapshot()
|
||||
{
|
||||
lock (m_Lock)
|
||||
{
|
||||
return new List<ConsoleLogEntryDto>(m_Entries);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Number of entries currently retained in the buffer.</summary>
|
||||
public static int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (m_Lock)
|
||||
{
|
||||
return m_Entries.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Drop all captured entries.</summary>
|
||||
public static void Clear()
|
||||
{
|
||||
lock (m_Lock)
|
||||
{
|
||||
m_Entries.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d212c06f2c819e04c892f7e41520f975
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using Unity.Pipeline.Commands;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Profiling;
|
||||
|
||||
namespace Unity.Pipeline.Editor.Commands.Observability
|
||||
{
|
||||
/// <summary>
|
||||
/// Render statistics for the most recently rendered Editor frame, read from
|
||||
/// <see cref="UnityStats"/>. These reflect the last frame the Editor drew (Game/Scene view), not
|
||||
/// an averaged measurement — treat them as a snapshot.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class RenderStats
|
||||
{
|
||||
/// <summary>Draw calls issued for the last rendered frame.</summary>
|
||||
[JsonProperty("drawCalls")]
|
||||
public int DrawCalls { get; set; }
|
||||
|
||||
/// <summary>Total batches (dynamic + static + instanced) for the last rendered frame.</summary>
|
||||
[JsonProperty("batches")]
|
||||
public int Batches { get; set; }
|
||||
|
||||
/// <summary>SetPass calls (shader pass switches) for the last rendered frame.</summary>
|
||||
[JsonProperty("setPassCalls")]
|
||||
public int SetPassCalls { get; set; }
|
||||
|
||||
/// <summary>Triangles submitted for the last rendered frame.</summary>
|
||||
[JsonProperty("triangles")]
|
||||
public int Triangles { get; set; }
|
||||
|
||||
/// <summary>Vertices submitted for the last rendered frame.</summary>
|
||||
[JsonProperty("vertices")]
|
||||
public int Vertices { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process memory usage read from <see cref="Profiler"/>. Values are in bytes and are always
|
||||
/// available at runtime regardless of whether the Profiler window is open.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class MemoryStats
|
||||
{
|
||||
/// <summary>Total memory currently allocated by Unity (bytes).</summary>
|
||||
[JsonProperty("totalAllocatedBytes")]
|
||||
public long TotalAllocatedBytes { get; set; }
|
||||
|
||||
/// <summary>Total memory reserved by Unity (bytes).</summary>
|
||||
[JsonProperty("totalReservedBytes")]
|
||||
public long TotalReservedBytes { get; set; }
|
||||
|
||||
/// <summary>Mono managed heap memory in use (bytes).</summary>
|
||||
[JsonProperty("monoUsedBytes")]
|
||||
public long MonoUsedBytes { get; set; }
|
||||
|
||||
/// <summary>Mono managed heap size reserved (bytes).</summary>
|
||||
[JsonProperty("monoHeapBytes")]
|
||||
public long MonoHeapBytes { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Latest CPU/GPU frame timing from <see cref="FrameTimingManager"/>. The Frame Timing Manager
|
||||
/// must have a timing captured for <see cref="Available"/> to be true; when unavailable the
|
||||
/// timing fields are left at zero.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class FrameTimingStats
|
||||
{
|
||||
/// <summary>True when a frame timing was captured and the milliseconds fields are meaningful.</summary>
|
||||
[JsonProperty("available")]
|
||||
public bool Available { get; set; }
|
||||
|
||||
/// <summary>Total CPU frame time in milliseconds.</summary>
|
||||
[JsonProperty("cpuFrameTimeMs")]
|
||||
public double CpuFrameTimeMs { get; set; }
|
||||
|
||||
/// <summary>Total GPU frame time in milliseconds.</summary>
|
||||
[JsonProperty("gpuFrameTimeMs")]
|
||||
public double GpuFrameTimeMs { get; set; }
|
||||
|
||||
/// <summary>CPU main-thread frame time in milliseconds.</summary>
|
||||
[JsonProperty("cpuMainThreadFrameTimeMs")]
|
||||
public double CpuMainThreadFrameTimeMs { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Composite performance snapshot returned by <c>get_performance_stats</c>: render counters,
|
||||
/// process memory, and the latest frame timing.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class PerformanceStats
|
||||
{
|
||||
/// <summary>Render counters for the last rendered Editor frame.</summary>
|
||||
[JsonProperty("render")]
|
||||
public RenderStats Render { get; set; }
|
||||
|
||||
/// <summary>Process memory usage at capture time.</summary>
|
||||
[JsonProperty("memory")]
|
||||
public MemoryStats Memory { get; set; }
|
||||
|
||||
/// <summary>Latest CPU/GPU frame timing, if one was captured.</summary>
|
||||
[JsonProperty("frameTiming")]
|
||||
public FrameTimingStats FrameTiming { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read-only telemetry command (CLI-198) that snapshots render, memory, and frame-timing stats so
|
||||
/// an agent can reason about Editor performance without opening the Profiler or Stats overlay.
|
||||
/// </summary>
|
||||
public static class PerformanceCommands
|
||||
{
|
||||
[CliCommand("get_performance_stats", "Read render, memory, and frame-timing stats (structured, read-only).")]
|
||||
public static PerformanceStats GetPerformanceStats()
|
||||
{
|
||||
return new PerformanceStats
|
||||
{
|
||||
Render = new RenderStats
|
||||
{
|
||||
// UnityStats reflects the last frame rendered by the Editor (Game/Scene view).
|
||||
DrawCalls = UnityStats.drawCalls,
|
||||
Batches = UnityStats.dynamicBatches + UnityStats.staticBatches + UnityStats.instancedBatches,
|
||||
SetPassCalls = UnityStats.setPassCalls,
|
||||
Triangles = UnityStats.triangles,
|
||||
Vertices = UnityStats.vertices
|
||||
},
|
||||
Memory = new MemoryStats
|
||||
{
|
||||
TotalAllocatedBytes = Profiler.GetTotalAllocatedMemoryLong(),
|
||||
TotalReservedBytes = Profiler.GetTotalReservedMemoryLong(),
|
||||
MonoUsedBytes = Profiler.GetMonoUsedSizeLong(),
|
||||
MonoHeapBytes = Profiler.GetMonoHeapSizeLong()
|
||||
},
|
||||
FrameTiming = CaptureFrameTiming()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Capture the latest CPU/GPU frame timing. The Frame Timing Manager only yields data once a
|
||||
/// timing has been captured (and platform support exists), so any failure or empty result is
|
||||
/// reported as <see cref="FrameTimingStats.Available"/> = false rather than throwing.
|
||||
/// </summary>
|
||||
private static FrameTimingStats CaptureFrameTiming()
|
||||
{
|
||||
try
|
||||
{
|
||||
FrameTimingManager.CaptureFrameTimings();
|
||||
|
||||
var timings = new FrameTiming[1];
|
||||
uint received = FrameTimingManager.GetLatestTimings(1, timings);
|
||||
if (received > 0)
|
||||
{
|
||||
return new FrameTimingStats
|
||||
{
|
||||
Available = true,
|
||||
CpuFrameTimeMs = timings[0].cpuFrameTime,
|
||||
GpuFrameTimeMs = timings[0].gpuFrameTime,
|
||||
CpuMainThreadFrameTimeMs = timings[0].cpuMainThreadFrameTime
|
||||
};
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Frame timing is platform-dependent and may be unsupported; report as unavailable.
|
||||
}
|
||||
|
||||
return new FrameTimingStats { Available = false };
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ae9262a38e20733409c3a084cb810bdc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user