Compare commits
3
Commits
b1a160d159
...
96680fd4c6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96680fd4c6 | ||
|
|
432ed2a70c | ||
|
|
186eec4e75 |
@@ -0,0 +1,47 @@
|
|||||||
|
using Common.Attributes;
|
||||||
|
using Common.Tools;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Command
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 步骤运行耗时统计命令。
|
||||||
|
/// <para>
|
||||||
|
/// 命令库为纯 .NET 类库,不直接引用宿主的步骤管理器类型;
|
||||||
|
/// 实际的步骤数据查询能力由宿主(StepRunning)在执行测试步骤前通过
|
||||||
|
/// <see cref="Common.Tools.StepRuntimeContext.StepTimeQuery"/> 注入(依赖倒置),
|
||||||
|
/// 多台架并发运行时各作用域的查询器随异步流隔离,互不串扰。
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
[ADPCommand]
|
||||||
|
public static class CommandRunningTime
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 计算测试步骤运行时间之和:统计步骤管理器主程序中,
|
||||||
|
/// 序号从起始序号到结束序号(含两端)范围内所有步骤的运行耗时总和。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="起始序号">起始步骤序号(从 1 开始)</param>
|
||||||
|
/// <param name="结束序号">结束步骤序号(含本身)</param>
|
||||||
|
/// <returns>范围内步骤运行时间之和(毫秒);序号超界、范围内无步骤或查询异常时输出 0</returns>
|
||||||
|
public static double 获取步骤运行时间之和(int 起始序号, int 结束序号)
|
||||||
|
{
|
||||||
|
// 读取当前异步执行流注入的步骤耗时查询器(由 StepRunning 设置)
|
||||||
|
var query = StepRuntimeContext.StepTimeQuery.Value;
|
||||||
|
if (query == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[步骤耗时] 宿主未注入步骤耗时查询器,无法计算序号 {起始序号}~{结束序号} 的运行时间之和");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return query(起始序号, 结束序号);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[步骤耗时] 计算序号 {起始序号}~{结束序号} 的运行时间之和失败: {ex.Message}");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace Common.Tools
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 步骤运行时上下文桥接器(命令库与宿主之间的依赖倒置通道)。
|
||||||
|
/// <para>
|
||||||
|
/// 命令库(Command 项目)不引用宿主类型,无法直接访问步骤管理器的数据;
|
||||||
|
/// 宿主(StepRunning)在执行测试步骤前,将当前作用域的步骤耗时查询委托写入
|
||||||
|
/// <see cref="StepTimeQuery"/>;命令库中的耗时统计命令在执行时读取该委托完成查询。
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// 使用 <see cref="AsyncLocal{T}"/> 承载:委托随异步执行流(ExecutionContext)流动,
|
||||||
|
/// 多台架并发运行时各自持有自己作用域的查询器,天然隔离互不串扰。
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public static class StepRuntimeContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 当前异步执行流的步骤耗时查询委托:(起始序号, 结束序号) → 该范围内步骤运行时间之和(毫秒)。
|
||||||
|
/// 由宿主(StepRunning)在每次执行测试步骤方法前设置;未设置时命令侧按未注入处理。
|
||||||
|
/// </summary>
|
||||||
|
[Browsable(false)]
|
||||||
|
public static readonly AsyncLocal<Func<int, int, double>?> StepTimeQuery = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -433,7 +433,9 @@ namespace MonitorModule.ViewModels
|
|||||||
.Where(m =>
|
.Where(m =>
|
||||||
{
|
{
|
||||||
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
|
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
|
||||||
if (m.ReturnType != typeof(Task<string>)) return false;
|
// 同时支持 Task<string> 和 Task<double> 返回类型
|
||||||
|
var rt = m.ReturnType;
|
||||||
|
if (rt != typeof(Task<string>) && rt != typeof(Task<double>)) return false;
|
||||||
var parms = m.GetParameters();
|
var parms = m.GetParameters();
|
||||||
return parms.Length == 0 ||
|
return parms.Length == 0 ||
|
||||||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
||||||
|
|||||||
@@ -233,7 +233,9 @@ namespace MonitorModule.ViewModels
|
|||||||
.Where(m =>
|
.Where(m =>
|
||||||
{
|
{
|
||||||
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
|
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
|
||||||
if (m.ReturnType != typeof(Task<string>)) return false;
|
// 同时支持 Task<string> 和 Task<double> 返回类型
|
||||||
|
var rt = m.ReturnType;
|
||||||
|
if (rt != typeof(Task<string>) && rt != typeof(Task<double>)) return false;
|
||||||
var parms = m.GetParameters();
|
var parms = m.GetParameters();
|
||||||
return parms.Length == 0 ||
|
return parms.Length == 0 ||
|
||||||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ namespace UIShare.GlobalVariable
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 已注册的采样项:(Fingerprint, MethodName, 编译后的委托, DeviceInstance)
|
/// 已注册的采样项:(Fingerprint, MethodName, 编译后的委托, DeviceInstance)
|
||||||
/// 委托签名统一为 Func<object, CancellationToken, Task<string>>
|
/// 委托签名统一为 Func<object, CancellationToken, Task<double>>
|
||||||
/// - 第一个参数 = 设备实例(open delegate 风格,编译时已做类型转换)
|
/// - 第一个参数 = 设备实例(open delegate 风格,编译时已做类型转换)
|
||||||
/// - 第二个参数 = CancellationToken(无参方法会忽略它)
|
/// - 第二个参数 = CancellationToken(无参方法会忽略它)
|
||||||
/// - 返回值 = Task<string>
|
/// - 返回值 = Task<double>(Task<string> 方法自动 TryParse 为 double)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly List<(string Fingerprint, string MethodName, Func<object, CancellationToken, Task<string>> Invoker, object Device)> _registeredMethods = new();
|
private readonly List<(string Fingerprint, string MethodName, Func<object, CancellationToken, Task<double>> Invoker, object Device)> _registeredMethods = new();
|
||||||
|
|
||||||
private CancellationTokenSource? _cts;
|
private CancellationTokenSource? _cts;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
@@ -76,11 +76,13 @@ namespace UIShare.GlobalVariable
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将 MethodInfo 编译为强类型委托,后续调用零反射开销。
|
/// 将 MethodInfo 编译为强类型委托,后续调用零反射开销。
|
||||||
/// 统一签名为 Func<object, CancellationToken, Task<string>>:
|
/// 统一签名为 Func<object, CancellationToken, Task<double>>:
|
||||||
|
/// - Task<double> 方法:直接返回 double 值
|
||||||
|
/// - Task<string> 方法:自动 TryParse 为 double
|
||||||
/// - 无参方法:忽略 CancellationToken
|
/// - 无参方法:忽略 CancellationToken
|
||||||
/// - 带 CancellationToken 方法:直接透传
|
/// - 带 CancellationToken 方法:直接透传
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static Func<object, CancellationToken, Task<string>> BuildInvoker(MethodInfo method)
|
private static Func<object, CancellationToken, Task<double>> BuildInvoker(MethodInfo method)
|
||||||
{
|
{
|
||||||
var deviceParam = Expression.Parameter(typeof(object), "device");
|
var deviceParam = Expression.Parameter(typeof(object), "device");
|
||||||
var ctParam = Expression.Parameter(typeof(CancellationToken), "ct");
|
var ctParam = Expression.Parameter(typeof(CancellationToken), "ct");
|
||||||
@@ -96,9 +98,25 @@ namespace UIShare.GlobalVariable
|
|||||||
|
|
||||||
var call = Expression.Call(castDevice, method, callArgs);
|
var call = Expression.Call(castDevice, method, callArgs);
|
||||||
|
|
||||||
return Expression.Lambda<Func<object, CancellationToken, Task<string>>>(
|
if (method.ReturnType == typeof(Task<double>))
|
||||||
|
{
|
||||||
|
// Task<double> 方法:直接返回
|
||||||
|
return Expression.Lambda<Func<object, CancellationToken, Task<double>>>(
|
||||||
call, deviceParam, ctParam).Compile();
|
call, deviceParam, ctParam).Compile();
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Task<string> 方法:await 后 TryParse 为 double
|
||||||
|
return async (device, ct) =>
|
||||||
|
{
|
||||||
|
// 通过 MethodInfo.Invoke 调用(仅 Task<string> 路径,性能可接受)
|
||||||
|
var task = (Task<string>)method.Invoke(device,
|
||||||
|
parms.Length == 1 ? new object[] { ct } : Array.Empty<object>())!;
|
||||||
|
string raw = await task.ConfigureAwait(false);
|
||||||
|
return double.TryParse(raw, out double v) ? v : double.NaN;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 扫描 GlobalInfo.HardwarePool 中所有已创建的物理设备,
|
/// 扫描 GlobalInfo.HardwarePool 中所有已创建的物理设备,
|
||||||
@@ -126,7 +144,9 @@ namespace UIShare.GlobalVariable
|
|||||||
.Where(m =>
|
.Where(m =>
|
||||||
{
|
{
|
||||||
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
|
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
|
||||||
if (m.ReturnType != typeof(Task<string>)) return false;
|
// 同时支持 Task<string> 和 Task<double> 返回类型
|
||||||
|
var rt = m.ReturnType;
|
||||||
|
if (rt != typeof(Task<string>) && rt != typeof(Task<double>)) return false;
|
||||||
var parms = m.GetParameters();
|
var parms = m.GetParameters();
|
||||||
return parms.Length == 0 ||
|
return parms.Length == 0 ||
|
||||||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
||||||
@@ -184,10 +204,10 @@ namespace UIShare.GlobalVariable
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 直接委托调用,零反射开销
|
// 直接委托调用,零反射开销(Task<double> 路径)或自动 TryParse(Task<string> 路径)
|
||||||
string raw = await entry.Invoker(entry.Device, token).ConfigureAwait(false);
|
double value = await entry.Invoker(entry.Device, token).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!double.TryParse(raw, out double value)) return;
|
if (double.IsNaN(value)) return;
|
||||||
|
|
||||||
// 采样成功,重置失败计数
|
// 采样成功,重置失败计数
|
||||||
_failureCounts.TryRemove(channelKey, out _);
|
_failureCounts.TryRemove(channelKey, out _);
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ namespace UIShare
|
|||||||
ResetAllStepStatus(program.ErrorStepCollection);
|
ResetAllStepStatus(program.ErrorStepCollection);
|
||||||
tmpParameters.Clear();
|
tmpParameters.Clear();
|
||||||
TestRoundID = Guid.NewGuid();
|
TestRoundID = Guid.NewGuid();
|
||||||
|
// 注入步骤耗时查询器:AsyncLocal 随异步执行流流动,命令库(CommandRunningTime)据此查询当前作用域主程序的步骤耗时
|
||||||
|
StepRuntimeContext.StepTimeQuery.Value = QueryStepRunTimeSum;
|
||||||
}
|
}
|
||||||
foreach (var item in program.Parameters)
|
foreach (var item in program.Parameters)
|
||||||
{
|
{
|
||||||
@@ -261,6 +263,8 @@ namespace UIShare
|
|||||||
tmpParameters.Clear();
|
tmpParameters.Clear();
|
||||||
testItemStack.Clear();
|
testItemStack.Clear();
|
||||||
TestRoundID = Guid.NewGuid();
|
TestRoundID = Guid.NewGuid();
|
||||||
|
// 注入步骤耗时查询器:AsyncLocal 随异步执行流流动,命令库(CommandRunningTime)据此查询当前作用域主程序的步骤耗时
|
||||||
|
StepRuntimeContext.StepTimeQuery.Value = QueryStepRunTimeSum;
|
||||||
}
|
}
|
||||||
int initialLoopStackCount = loopStack.Count;
|
int initialLoopStackCount = loopStack.Count;
|
||||||
foreach (var item in program.Parameters)
|
foreach (var item in program.Parameters)
|
||||||
@@ -469,7 +473,11 @@ namespace UIShare
|
|||||||
{
|
{
|
||||||
if(_scopedContext.Program.StepCollection.Count>1)
|
if(_scopedContext.Program.StepCollection.Count>1)
|
||||||
_scopedContext.SelectedStep = null;
|
_scopedContext.SelectedStep = null;
|
||||||
|
// 区间延迟不计入步骤运行时间:延迟期间暂停步骤计时器(子程序内的区间延迟同样排除,它们都归属顶层步骤的计时区间)
|
||||||
|
bool timingPaused = stepStopwatch.IsRunning;
|
||||||
|
if (timingPaused) stepStopwatch.Stop();
|
||||||
await Task.Delay(_systemConfig.PerformanceLevel, cancellationToken);
|
await Task.Delay(_systemConfig.PerformanceLevel, cancellationToken);
|
||||||
|
if (timingPaused) stepStopwatch.Start();
|
||||||
|
|
||||||
|
|
||||||
// 1. 查找类型
|
// 1. 查找类型
|
||||||
@@ -800,6 +808,30 @@ namespace UIShare
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询当前作用域主程序步骤集合中,序号位于 [起始序号, 结束序号](含两端)范围内步骤的运行时间之和(毫秒)。
|
||||||
|
/// <para>由 ExecuteSteps / ExecuteErrorSteps 顶层启动时经 StepRuntimeContext 注入(AsyncLocal 随异步流流动),
|
||||||
|
/// 供命令库 CommandRunningTime.获取步骤运行时间之和 调用(依赖倒置:命令库不引用宿主类型)。</para>
|
||||||
|
/// <para>序号倒置、超界(范围内不存在步骤)时记录错误日志并返回 0;未执行步骤的耗时按 0 计入。</para>
|
||||||
|
/// </summary>
|
||||||
|
private double QueryStepRunTimeSum(int startIndex, int endIndex)
|
||||||
|
{
|
||||||
|
var steps = _scopedContext.Program.StepCollection;
|
||||||
|
if (startIndex > endIndex)
|
||||||
|
{
|
||||||
|
LoggerHelper.Error($"[步骤耗时] 查询失败:起始序号 {startIndex} 大于结束序号 {endIndex},返回 0");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
var matched = steps.Where(s => s.Index >= startIndex && s.Index <= endIndex).ToList();
|
||||||
|
if (matched.Count == 0)
|
||||||
|
{
|
||||||
|
LoggerHelper.Error($"[步骤耗时] 查询失败:序号 {startIndex}~{endIndex} 范围内不存在测试步骤(主程序共 {steps.Count} 步),返回 0");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// 尚未执行或未启用的步骤 RunTime 为 null,按 0 计入
|
||||||
|
return matched.Sum(s => s.RunTime ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateCurrentStepResult(StepVM step, bool paraResult = true, bool stepResult = true, int depth = 0)
|
private void UpdateCurrentStepResult(StepVM step, bool paraResult = true, bool stepResult = true, int depth = 0)
|
||||||
{
|
{
|
||||||
if (stepResult && paraResult)
|
if (stepResult && paraResult)
|
||||||
|
|||||||
Reference in New Issue
Block a user