Releases: inversionhourglass/Rougamo
v5.0.0
5.0.0
性能优化
切面类型属性成员缩减
成员缩减以减少切面类型实例化后内存占用,将目前IMo
中定义的所有属性全部删除,分别提供相应的Attribute
和接口,具体改动如下:
5.0 之前切面类型属性 | 5.0 对应的Attribute | 5.0 对应的Interface |
---|---|---|
Flags | PointcutAttribute | IFlexibleModifierPointcut |
Pattern | PointcutAttribute | IFlexiblePatternPointcut |
Features | AdviceAttribute | / |
MethodContextOmits | OptimizationAttribute | / |
ForceSync | OptimizationAttribute | / |
Order | / | IFlexibleOrderable |
Q: 为什么升级后有的只有Attribute,有的只有接口,而有的两个都有
A: 在5.0之前的版本可以通过new
关键字为属性增加setter,然后在应用切面类型时通过属性动态配置(如下代码所示)。这种方式在5.0版本中默认不支持,但可以通过上表中的接口实现,对于没有提供相应接口的属性是经过考虑后认为不会在应用切面类型时指定的。对于Attribute,从上表中可以看到,只有Order
属性没有提供对应的Attribute,这是因为考虑到Order
一般都是应用切面类型时动态指定,当然,如果你希望指定默认值,同样可以实现IFlexibleOrderable
接口后指定默认值。
// 5.0之前的用法
public class TestAttribute : MoAttribute
{
// 默认Pattern只有getter,通过new关键字为Pattern增加setter
public new Pattern { get; set; }
}
[Test(Pattern = "method(* *.Try*(..))")] // 应用Attribute动态指定Pattern
public class Cls { }
// 5.0之后实现应用Attribute时动态指定Pattern属性
public class TestAttribute : MoAttribute, IFlexiblePatternPointcut
{
public string Pattern { get; set; }
}
下面展示 5.0 升级前后的差异:
// 5.0之前的切面类型定义
public class TestAttribute : MoAttribute
{
public override AccessFlags Flags => AccessFlags.All | AccessFlags.Method;
public override string Pattern => "method(* *(..))";
public override Feature Features => Feature.OnEntry;
public override ForceSync ForceSync => ForceSync.All;
public override Omit MethodContextOmits => Omit.None;
public override double Order => 2;
}
// 5.0的切面类型定义
[Pointcut("method(* *(..))")] // 由于pattern的优先级高于修饰符Flgas,所以这里只保留了pattern
[Advice(Feature.OnEntry)]
[Optimization(ForceSync = ForceSync.All, MethodContext = Omit.None)]
public class T1Attribute : MoAttribute, IFlexibleOrderable
{
public double Order { get; set; } = 2;
}
Roslyn代码分析器
本次的属性成员变动较大,升级后手动修改会比较繁琐,同时还可能出现遗漏。虽然Rougamo在编译时会对切面类型进行检查,并在发现不符合规范的切面类型时产生一个编译错误。但 5.0 提供了更好的升级体验,新增 Roslyn 代码分析器和代码修复程序,可以在编写代码时直接发现问题并提供快捷修复。
ref struct参数及返回值处理
由于ref struct
无法进行装箱/拆箱操作,所以ref struct
参数和返回值无法保存到MethodContext
中,5.0 之前推荐通过Omit.Arguments
和Omit.ReturnValue
处理该问题,详见 #61.
SkipRefStructAttribute
在 5.0 版本中新增SkipRefStructAttribute
用于处理ref struct
:
[SkipRefStruct]
[Test]
public ReadOnlySpan<char> M(ReadOnlySpan<char> value) => default;
这样的优势在于,如果方法上应用了多个切面类型,不再需要为每个切面类型指定MethodContextOmits
,同时SkipRefStructAttribute
还可以应用在类和程序集上,可以在更大范围上声明忽略ref struct
。
配置项skip-ref-struct
在确定当前程序集默认忽略ref struct
的情况下,可以通过配置项skip-ref-struct
为当前程序集应用这个设定,配合 Cli4Fody 可以实现非侵入式配置,skip-ref-struct
设置为true
的效果等同于[assembly: SkipRefStruct]
。
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Rougamo skip-ref-struct="true" />
</Weavers>
自定义切面类型生命周期
在5.0版本中,可以通过LifetimeAttribute
指定切面类型的生命周期,通过Pooled
和Singleton
生命周期可以优化Rougamo产生的GC.
[Lifetime(Lifetime.Transient)] // 临时,每次创建都是直接new。在没有应用LifetimeAttribute时,默认为Transient
public class Test1Attribute : MoAttribute { }
[Lifetime(Lifetime.Pooled)] // 对象池,每次创建都从对象池中获取
public class Test2Attribute : MoAttribute { }
[Lifetime(Lifetime.Singleton)] // 单例
public class Test3Attribute : MoAttribute { }
需要注意的是,Pooled
和Singleton
生命周期要求切面类型必须包含无参构造方法,另外Singleton
还要求不能在应用切面类型Attribute时动态指定属性,避免并发问题。
IResettable
Pooled
生命周期使用对象池,要求对象在返回对象池之前要重置自身状态。重置操作可以直接在OnExit
中做,或者更明确的实现IResettable
,在TryReset
中进行重置操作。
[Lifetime(Lifetime.Pooled)]
public class PooledAttribute : MoAttribute, IResettable
{
public SingletonAttribute() { }
public int X { get; set; }
public override void OnExit(MethodContext context)
{
// 可以在OnExit中状态重置,比如将X重置为0
X = 0;
}
public bool TryReset()
{
// 也可以实现IResettable接口,在该方法中完成状态重置
X = 0;
// 返回true表示重置成功,返回false,当前对象将会直接抛弃,不会返回到对象池中
return true;
}
}
MethodContext对象池化
在 5.0 版本中,MethodContext
将默认从对象池中获取,这一默认行为将在较大程度上优化Rougamo产生的GC。
MethodContext
的对象池和切面类型的对象池用的是同一个,可以通过环境变量设置对象池最大持有数量,默认为CPU逻辑核心数 * 2
(不同类型分开)。
环境变量 | 说明 |
---|---|
NET_ROUGAMO_POOL_MAX_RETAIN |
对象池最大持有对象数量,对所有类型生效 |
NET_ROUGAMO_POOL_MAX_RETAIN_<TypeFullName> |
指定类型对象池最大持有对象数量,覆盖NET_ROUGAMO_POOL_MAX_RETAIN 配置,<TypeFullName> 为指定类型的全名称,命名空间分隔符. 替换为_ |
异常堆栈信息优化
关联 [#82]
Rougamo 自 4.0 版本开始全面使用代理织入的方式,由于该方式会为被拦截方法额外生成一个代理方法,所以在堆栈信息中会额外产生一层调用堆栈,在程序抛出异常时,调用堆栈会显得复杂且冗余:
// 测试代码
public class TestAttribute : MoAttribute { }
try
{
await M1();
}
catch (Exception e)
{
Console.WriteLine(e);
}
[Test]
public static async Task M1() => await M2();
[Test]
public static async ValueTask M2()
{
await Task.Yield();
M3();
}
[Test]
public static void M3() => throw new NotImplementedException();
Rougamo 5.0之前,上面代码在.NET6.0中运行的结果为(不同.NET版本堆栈信息可能有些差异):
System.NotImplementedException: The method or operation is not implemented.
at X.Program.$Rougamo_M3() in D:\X\Y\Z\Program.cs:line 49
at X.Program.M3()
at X.Program.$Rougamo_M2() in D:\X\Y\Z\Program.cs:line 43
at X.Program.M2()
at X.Program.M2()
at X.Program.$Rougamo_M1() in D:\X\Y\Z\Program.cs:line 36
at X.Program.M1()
at X.Program.M1()
at X.Program.Main(String[] args) in D:\X\Y\Z\Program.cs:line 13
Rougamo 5.0版本之后,运行结果为:
System.NotImplementedException: The method or operation is not implemented.
at X.Program.$Rougamo_M3() in D:\X\Y\Z\Program.cs:line 49
at X.Program.$Rougamo_M2() in D:\X\Y\Z\Program.cs:line 43
at X.Program.$Rougamo_M1() in D:\X\Y\Z\Program.cs:line 36
at X.Program.Main(String[] args) in D:\X\Y\Z\Program.cs:line 13
这种异常堆栈优化在.NET6.0及之后的.NET版本中是默认的,不需要任何操作,但对于 .NET 6.0 之前的版本,需要调用Exception
的扩展方法ToNonRougamoString
来获取优化后的ToString字符串,或者调用Exception
的扩展方法GetNonRougamoStackTrace
获取优化后的调用堆栈。
之所以.NET6.0之后默认支持异常堆栈优化,是因为.NET6.0之后调用堆栈会默认排除应用了StackTraceHiddenAttribute
的方法。
关于优化后堆栈信息默认方法名自带$Rougamo_
前缀的说明
代理织入使得实际方法全部增加$Rougamo_
前缀,只有实际方法与PDB信息对应,可以获取行号等信息。不做额外处理去除前缀是因为没有必要,前缀固定不会影响阅读分析,额外操作去除前缀影响性能,同时也会导致 .NET 6.0 及以上版本无法无感知完成优化。如果确实想要去除该前缀,请自行处理。
另外,如果你有特殊需求不需要这种堆栈信息优化,可以将配置项pure-stacktrace
设置为false
。
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Rougamo pure-stacktrace="false" />
</Weavers>
AspectN新语法
新增ctor
和cctor
,用于用于快速匹配构造方法和静态构造方法。
-
ctor(<declaring-type>([<parameter-type>..]))
// 匹配所有构造方法 [Pointcut("ctor(*(..))")] // 匹配所有非泛型类型的构造方法 [Pointcut("ctor(*<!>(..))")] // 匹配IService子类的构造方法 [Pointcut("ctor(IService+(..))")] // 匹配所有无参构造方法 [Pointcut("ctor(*())")] // 匹配所有包含三个参数(任意类型)的构造方法 [Pointcut("ctor(*(,,))")] // 匹配两个参数分别为int和Guid的构造方法 [Pointcut("ctor(*(int,System.Guid))")]
-
cctor(<declaring-type>)
// 匹配所有静态构造方法 [Pointcut("cctor(*)")] // 匹配类名包含Singleton的静态构造方法 [Pointcut("cctor(*Singleton*)")]
配置化非侵入式织入
5.0 可以通过配置FodyWeavers.xml
完成切面类型应用,而不必再添加/修改C#代码。
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Rougamo>
<Mos>
<Mo assembly="Rougamo.OpenTelemetry" type="Rougamo.OpenTelemetry.OtelAttribute" pattern="method(* *Service.*(..))"/>
</Mos>
</Rougamo>
</Weavers>
在上面的例子中,OtelAttribute
将被应用到类名以Service
结尾的所有方法上。
上面的配置中,每一个Mo
节点为一条应用规则,Mos
节点下可以定义多个Mo
节点,下面是Mo
节点的属性说明:
type
,切面类型全名称assembly
,切面类型所在程序集名称(不要.dll后缀)pattern
,AspectN表达式,type
将应用到该表达式匹配的方法上。该配置可选,未设置时将采用切面类型type
自身的配置
配置结合 Cli4Fody 可以实现零侵入式的代码织入,详细可参考 Pooling 与 Cli4Fody 的 零侵入实践.
其他
-
删除配置项
moarray-threshold
该配置项是用数组优化大量切面类型应用到方法时,用遍历数组执行切面方法的方式代替每个切面类型单独执行切面方法,以达到精简MSIL的目的。
但随着Rougamo的功能完善,在4.0版本中因异步切面的加入,使得异步方法无法使用数组达到预期优化。在5.0版本中,随着对象池的加入,同步方法也难以使用数组完成预期优化。
综合复杂度和实际优化效果考虑,最终决定在5.0版本中移除配置项
moarray-threshold
。 -
新增ISyncMo和IAsyncMo接口
由于结构体无法继承父类/父结构体,所以在定义结构体切面类型时只能直接实现
IMo
接口,但该接口包含全部同步/异步切面方法,全部实现比较繁琐。Rougamo在 5.0 版本中新增
ISyncMo
和IAsyncMo
,通过 默认接口方法 对部分方法提供默认实现。默认接口方法需要 SDK 最低 .NET Core 3.0 的版本,所以只有 .NET Core 3.0 及以上版本才有
ISyncMo
和IAsyncMo
两个接口。// 实现ISyncMo接口可以不用实现异步切面方法 [Pointcut("method(* *(..))")] public struct SyncMo : ISyncMo { public void OnEntry(MethodContext context) { } public void OnException(MethodContext context) { } public void OnExit(MethodContext context)...
v4.0.4
v4.0.3
v4.0.2
v4.0.1
- 修复 [#77]
修复描述
异步方法的调试信息不仅需要对应的DebugInformation和CustomDebugInformation以及一些Attribute,生成的StateMachine类名以及变量对应的字段名也有一定格式要求,具体参考:
- 类名生成:https://github.com/dotnet/roslyn/blob/5cba0ce666766b1db7cd75009575c7e12c4be72c/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNames.cs#L78-L84
- 字段名生成:https://github.com/dotnet/roslyn/blob/5cba0ce666766b1db7cd75009575c7e12c4be72c/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNames.cs#L203-L244
- 字段solt计算:https://github.com/dotnet/roslyn/blob/5cba0ce666766b1db7cd75009575c7e12c4be72c/src/Compilers/CSharp/Portable/Lowering/StateMachineRewriter/StateMachineRewriter.cs#L194-L197
v4.0.0
- 新增异步切面功能
IMo
新增系列方法OnEntryAsync
,OnExceptionAsync
,OnSuccessAsync
,OnExitAsync
,当将肉夹馍应用到异步方法上时将调用对应的OnXxxAsync
异步切面方法- 新增
AsyncMoAttribute
,原有的MoAttribute
提供了默认的OnXxxAsync
实现并且不支持重写,而AsyncMoAttribute
相反的提供了OnXxx
的默认实现,同时可以重写OnXxxAsync
系列方法 - 新增
RawMoAttribute
,同时支持重写同步切面的OnXxx
和异步切面的OnXxxAsync
,但需要自己注意同步切面方法与异步切面方法的调用关系,不可产生递归调用 - 新增
RawMo
,Mo
,AsyncMo
,对应RawMoAttribute
,MoAttribute
,AsyncMoAttribute
,区别在于前者仅实现IMo
接口,没有继承Attribute
IMo
新增ForceSync
属性,可用于指定在异步方法上强制执行同步切面方法,用于性能优化
- [#68] 在async void上应用Rougamo时,编译时在输出告警信息到MSBuild,可以通过在项目文件的
PropertyGroup
节点下增加<FodyTreatWarningsAsErrors>true</FodyTreatWarningsAsErrors>
配置,将告警信息变为错误信息,强制编译失败来避免async void
的使用 - 异步方法在编织时将忽略
moarray-threshold
配置项,无论存在多少个IMo
对象,都不会使用数组进行存储,因为异步切面的代码并不会因为使用数组而得到优化,反而会产生更多的操作指令。该配置对于同步方法依旧生效 - 优化ref struct参数/返回值的报错提示,一次编译会检查所有方法,并产生多个错误信息到错误列表(Error List)
- 肉夹馍产生的错误列表(Error List)中的错误信息都可以直接双击定位到对应产生该错误的方法,方便排查和反馈问题
- 删除
MethodContext
中的IsAsync
,IsIterator
,MosNonEntryFIFO
,Data
属性,将RealReturnType
标记为过时并隐藏,同时新增TaskReturnType
属性,该属性与RealReturnType
具有类似功能 - 增加xcf文件,为
FodyWeavers.xml
中的Rougamo
节点增加智能提示功能
v3.1.0
v3.0.2
v3.0.1
- [#71] 修复blazor项目在发布时illink对程序集做裁减优化时产生的异常
程序集主要描述信息基本存储在ModuleDefinition的MetadataSystem字段中,各类型的原数据也基本从该对象中读取。然而通过删除/清空MethodDefinition的CustomDebugInformation并不会影响MetadataSystem中的数据,同时,在将修改写入程序集时,MetadataSystem中绝大部分数据都将直接清空后重新写入,然而CustomDebugInformation是个例外,这就导致MethodDefinition上对CustomDebugInformations的删除/清空操作无效。所以对CustomDebugInformation的删除操作还需要从MetadataSystem中的删除。参考 MetadataSystem.Clear()
- [#72] 修复Feature属性直接设置初始值的方式无效的问题
v3.0.0
- #36 应用Rougamo的方法支持步入调试
- #54 解决snupkg报checksum错误的问题,需直接依赖Fody,详见issue回复
- #60 支持自定义
AsyncMethodBuilder
- #63 支持泛型
Attribute<T>
- #65 修复特定
Type
类型无法作为MoAttribute
构造方法参数
3.0版本对比之前版本差异
3.0版本在代码织入的方式上进行了改变,从原方法内部进行代码织入的方式改为代理方法织入,这样的改变会使Rougamo之前提供的部分功能发生变化:
- 在 #53 中支持的功能“刷新参数值”现在仅对
out / ref
参数有效 void async
方法无法确保方法切实执行完毕后再执行OnSuccess
,也无法确保异常一定会进OnException
ExMoAttribute
弃用,代理调用的方式不再区分是否使用async / await
语法