Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e508c2c
docs(specs): Unity Perf Harness テスト容易化リファクタの設計を追加
OJII3 Jun 29, 2026
44c169e
docs(specs): Unity Perf Harness テスト容易化 spec の自己レビュー修正
OJII3 Jun 29, 2026
b9d1a52
docs(plans): Unity Perf Harness テスト容易化プランを追加
OJII3 Jun 29, 2026
7aeeb55
feat(perf): 4 interface (IPerfMetricsSink/ProfilerSampler/Clock/Stopw…
OJII3 Jun 29, 2026
c9da350
feat(perf): PerfJson.Escape 純関数ヘルパーを追加 (TDD)
OJII3 Jun 29, 2026
26de8f7
feat(perf): PerfJson.Write* メソッドを追加 (TDD)
OJII3 Jun 29, 2026
13754cb
feat(perf): MeasureDoneBuilder.BuildPublish を追加 (TDD)
OJII3 Jun 29, 2026
c6ec499
feat(perf): MeasureDoneBuilder.BuildSubscribe を追加 (TDD)
OJII3 Jun 29, 2026
91fd05e
feat(perf): PerfTransportDiagnostics / PerfSubscriptionDiagnostics 値オ…
OJII3 Jun 29, 2026
5935842
feat(perf): PerfDiagnosticsBuilder.BuildReceive を追加 (TDD)
OJII3 Jun 29, 2026
b3fe1ed
feat(perf): StopwatchPerfClock / StopwatchWrapper (IPerfClock 実装) を追加
OJII3 Jun 29, 2026
5076925
refactor(perf): PerfMetricsWriter を PerfJson 経由 + IPerfMetricsSink 実装…
OJII3 Jun 29, 2026
76f7317
refactor(perf): PerfProfilerRecorders を IPerfProfilerSampler 実装に
OJII3 Jun 29, 2026
d168578
refactor(perf): Run* の signature を 5 引数 (participant + 3 interface) に変更
OJII3 Jun 29, 2026
87a8ae9
test(perf): FakePerfMetricsSink/ProfilerSampler/Clock を追加
OJII3 Jun 29, 2026
002a213
test(perf): PerfRunFlowTests (Run* 統合) を追加 (TDD)
OJII3 Jun 29, 2026
c1e9070
fix(perf): RunRos2ToUnity で IPerfStopwatch を try/finally で dispose
OJII3 Jun 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Ros2Unity/Assets/Perf/IPerfClock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;

namespace ROSettaDDS.UnityPerfHarness
{
/// <summary>
/// 経過時間計測の抽象。テストでは fake に差し替え可能。
/// </summary>
internal interface IPerfClock
{
IPerfStopwatch Start();
}
}
2 changes: 2 additions & 0 deletions Ros2Unity/Assets/Perf/IPerfClock.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions Ros2Unity/Assets/Perf/IPerfMetricsSink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;

namespace ROSettaDDS.UnityPerfHarness
{
/// <summary>
/// perf run 中の event / sentinel 出力の抽象。テストでは fake に差し替え可能。
/// </summary>
internal interface IPerfMetricsSink : IDisposable
{
void Event(string name, IDictionary<string, object> fields = null);
void WriteSentinel(string path, string content);
}
}
2 changes: 2 additions & 0 deletions Ros2Unity/Assets/Perf/IPerfMetricsSink.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions Ros2Unity/Assets/Perf/IPerfProfilerSampler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;

namespace ROSettaDDS.UnityPerfHarness
{
/// <summary>
/// ProfilerRecorder のラッパ抽象。テストでは fake に差し替え可能。
/// </summary>
internal interface IPerfProfilerSampler : IDisposable
{
void Collect();
IDictionary<string, object> Snapshot();
}
}
2 changes: 2 additions & 0 deletions Ros2Unity/Assets/Perf/IPerfProfilerSampler.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions Ros2Unity/Assets/Perf/IPerfStopwatch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;

namespace ROSettaDDS.UnityPerfHarness
{
/// <summary>
/// Stopwatch の抽象。single-thread 想定。
/// Dispose は using 構文で使うため。実装 (StopwatchWrapper) は
/// 内部の System.Diagnostics.Stopwatch を所有しないため no-op。
/// </summary>
internal interface IPerfStopwatch : IDisposable
{
TimeSpan Elapsed { get; }
void Stop();
}
}
2 changes: 2 additions & 0 deletions Ros2Unity/Assets/Perf/IPerfStopwatch.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions Ros2Unity/Assets/Perf/MeasureDoneBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;

namespace ROSettaDDS.UnityPerfHarness
{
internal static class MeasureDoneBuilder
{
internal static IDictionary<string, object> BuildPublish(
TimeSpan elapsed,
int sent,
int serializedBytesPerMessage,
IDictionary<string, object> profilerFields)
{
var fields = new Dictionary<string, object>(profilerFields);
fields["elapsed_ms"] = elapsed.TotalMilliseconds;
fields["sent"] = sent;
fields["serialized_bytes_per_message"] = serializedBytesPerMessage;
fields["serialized_bytes"] = (long)serializedBytesPerMessage * sent;
fields["messages_per_second"] = sent / Math.Max(0.000001d, elapsed.TotalSeconds);
return fields;
}

internal static IDictionary<string, object> BuildSubscribe(
TimeSpan elapsed,
int received,
int serializedBytesPerMessage,
IDictionary<string, object> profilerFields,
IDictionary<string, object> diagnostics)
{
var fields = new Dictionary<string, object>(profilerFields);
fields["elapsed_ms"] = elapsed.TotalMilliseconds;
fields["received"] = received;
fields["serialized_bytes_per_message"] = serializedBytesPerMessage;
fields["serialized_bytes"] = (long)serializedBytesPerMessage * received;
fields["messages_per_second"] = received / Math.Max(0.000001d, elapsed.TotalSeconds);
if (diagnostics != null)
{
foreach (var kv in diagnostics)
{
fields[kv.Key] = kv.Value;
}
}
return fields;
}
}
}
2 changes: 2 additions & 0 deletions Ros2Unity/Assets/Perf/MeasureDoneBuilder.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 43 additions & 0 deletions Ros2Unity/Assets/Perf/PerfDiagnosticsBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Collections.Generic;

namespace ROSettaDDS.UnityPerfHarness
{
internal static class PerfDiagnosticsBuilder
{
internal static IDictionary<string, object> BuildReceive(
PerfTransportDiagnostics userUnicast,
PerfTransportDiagnostics userMulticast,
PerfSubscriptionDiagnostics subscription)
{
var fields = new Dictionary<string, object>();
AddTransport(fields, "user_unicast", userUnicast);
AddTransport(fields, "user_multicast", userMulticast);

fields["subscription_payloads_from_reader"] = subscription.PayloadsReceivedFromReader;
fields["subscription_messages_deserialized"] = subscription.MessagesDeserialized;
fields["subscription_deserialize_failures"] = subscription.DeserializeFailures;
fields["subscription_handler_invocations"] = subscription.HandlerInvocations;
fields["rtps_data_submessages_received"] = subscription.DataSubmessagesReceived;
fields["rtps_datafrag_submessages_received"] = subscription.DataFragSubmessagesReceived;
fields["rtps_reassembled_payloads"] = subscription.ReassembledPayloads;
fields["rtps_payloads_delivered"] = subscription.PayloadsDelivered;
fields["rtps_payloads_buffered_pending_match"] = subscription.PayloadsBufferedPendingMatch;
fields["rtps_payloads_dropped"] = subscription.PayloadsDropped;
return fields;
}

private static void AddTransport(
IDictionary<string, object> fields,
string prefix,
PerfTransportDiagnostics transport)
{
fields[prefix + "_transport_diagnostics_available"] = transport.Available;
if (!transport.Available) return;
fields[prefix + "_udp_datagrams_received"] = transport.DatagramsReceived;
fields[prefix + "_udp_datagrams_enqueued"] = transport.DatagramsEnqueued;
fields[prefix + "_udp_datagrams_dropped"] = transport.DatagramsDropped;
fields[prefix + "_udp_datagrams_dispatched"] = transport.DatagramsDispatched;
fields[prefix + "_udp_queue_count"] = transport.QueueCount;
}
}
}
2 changes: 2 additions & 0 deletions Ros2Unity/Assets/Perf/PerfDiagnosticsBuilder.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

73 changes: 73 additions & 0 deletions Ros2Unity/Assets/Perf/PerfJson.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System;
using System.Globalization;
using System.Text;

namespace ROSettaDDS.UnityPerfHarness
{
/// <summary>
/// Perf Metrics 用の JSON 純関数ヘルパー。
/// IL2CPP 起動時 overhead を避けるため手書き。System.Text.Json は使わない。
/// 仕様: \\ → \\\\, \" → \\\", \n → \\n, \r → \\r の 4 種のみ置換。
/// </summary>
internal static class PerfJson
{
internal static string Escape(string value)
{
if (value == null)
{
return string.Empty;
}
return value
.Replace("\\", "\\\\")
.Replace("\"", "\\\"")
.Replace("\n", "\\n")
.Replace("\r", "\\r");
}

internal static void WriteString(StringBuilder b, string key, string value, bool first = true)
{
if (!first) b.Append(',');
b.Append('"').Append(Escape(key)).Append("\":\"").Append(Escape(value)).Append('"');
}

internal static void WriteNumber(StringBuilder b, string key, long value, bool first = true)
{
AppendRaw(b, key, value.ToString(CultureInfo.InvariantCulture), first);
}

internal static void WriteBoolean(StringBuilder b, string key, bool value, bool first = true)
{
AppendRaw(b, key, value ? "true" : "false", first);
}

internal static void WriteValue(StringBuilder b, string key, object value, bool first = true)
{
if (value == null)
{
AppendRaw(b, key, "null", first);
}
else if (value is string text)
{
WriteString(b, key, text, first);
}
else if (value is bool flag)
{
AppendRaw(b, key, flag ? "true" : "false", first);
}
else if (value is int || value is long || value is float || value is double)
{
AppendRaw(b, key, Convert.ToString(value, CultureInfo.InvariantCulture), first);
}
else
{
WriteString(b, key, value.ToString(), first);
}
}

private static void AppendRaw(StringBuilder b, string key, string value, bool first)
{
if (!first) b.Append(',');
b.Append('"').Append(Escape(key)).Append("\":").Append(value);
}
}
}
2 changes: 2 additions & 0 deletions Ros2Unity/Assets/Perf/PerfJson.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

70 changes: 10 additions & 60 deletions Ros2Unity/Assets/Perf/PerfMetricsWriter.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using UnityEngine;

namespace ROSettaDDS.UnityPerfHarness
{
internal sealed class PerfMetricsWriter : IDisposable
internal sealed class PerfMetricsWriter : IPerfMetricsSink
{
private readonly PerfPlayerArguments _args;
private readonly StreamWriter _writer;
Expand All @@ -24,21 +23,21 @@ internal PerfMetricsWriter(PerfPlayerArguments args)
_writer.AutoFlush = true;
}

internal void Event(string name, IDictionary<string, object> fields = null)
public void Event(string name, IDictionary<string, object> fields = null)
{
var builder = new StringBuilder();
builder.Append('{');
AppendString(builder, "event", name, first: true);
AppendString(builder, "scenario", _args.Scenario);
AppendString(builder, "direction", _args.DirectionName);
AppendString(builder, "qos", _args.QosName);
AppendNumber(builder, "payload_bytes", _args.PayloadBytes);
AppendNumber(builder, "messages", _args.Messages);
PerfJson.WriteString(builder, "event", name, first: true);
PerfJson.WriteString(builder, "scenario", _args.Scenario, first: false);
PerfJson.WriteString(builder, "direction", _args.DirectionName, first: false);
PerfJson.WriteString(builder, "qos", _args.QosName, first: false);
PerfJson.WriteNumber(builder, "payload_bytes", _args.PayloadBytes, first: false);
PerfJson.WriteNumber(builder, "messages", _args.Messages, first: false);
if (fields != null)
{
foreach (var pair in fields)
{
AppendValue(builder, pair.Key, pair.Value);
PerfJson.WriteValue(builder, pair.Key, pair.Value, first: false);
}
}
builder.Append('}');
Expand All @@ -47,7 +46,7 @@ internal void Event(string name, IDictionary<string, object> fields = null)
Debug.Log(line);
}

internal void WriteSentinel(string path, string content)
public void WriteSentinel(string path, string content)
{
string directory = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(directory))
Expand All @@ -61,54 +60,5 @@ public void Dispose()
{
_writer.Dispose();
}

private static void AppendValue(StringBuilder builder, string key, object value)
{
if (value == null)
{
AppendRaw(builder, key, "null");
}
else if (value is string text)
{
AppendString(builder, key, text);
}
else if (value is bool flag)
{
AppendRaw(builder, key, flag ? "true" : "false");
}
else if (value is int || value is long || value is float || value is double)
{
AppendRaw(builder, key, Convert.ToString(value, CultureInfo.InvariantCulture));
}
else
{
AppendString(builder, key, value.ToString());
}
}

private static void AppendString(StringBuilder builder, string key, string value, bool first = false)
{
if (!first) builder.Append(',');
builder.Append('"').Append(Escape(key)).Append("\":\"").Append(Escape(value)).Append('"');
}

private static void AppendNumber(StringBuilder builder, string key, long value)
{
AppendRaw(builder, key, value.ToString(CultureInfo.InvariantCulture));
}

private static void AppendRaw(StringBuilder builder, string key, string value)
{
builder.Append(',').Append('"').Append(Escape(key)).Append("\":").Append(value);
}

private static string Escape(string value)
{
if (value == null)
{
return string.Empty;
}
return value.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r");
}
}
}
Loading