.NET8.0 AOT 經驗分享 FreeSql/FreeRedis/FreeScheduler 均已通過測試

2023-11-16 15:00:30

2023年11月15日,對.net的開發圈是一個重大的日子,.net 8.0正式版釋出。

圈內已經預熱了有半個月有餘,效能不斷超越,開發體驗越來越完美,早在.net 5.0的時候就各種吹風Aot編譯,直到6.0 7.0使用仍然比較麻煩,我個人比較期待本次更新的aot體驗。

有的群友幾個小時都等不了啦,半夜就開始更新預覽版,我是等到第二天早上正式布釋出才開始的,開機第一件事情下載.net8.0 SDK,隨後更新vs2022企業版。


支援開源

我是開源人:https://github.com/2881099

本文通過我們的開源專案升級,以及AOT試驗,記錄了整個經驗過程。

使用我們開源專案的朋友一般都知道,特點依賴較少(甚至零依賴),每次 .net 新版本釋出很輕鬆就支援了,並且為 AOT 埋下了種子。

第一個要更新的開源專案是FreeRedis,這個專案沒有任何外部依賴,本身是支援.net 8.0的,本次維護主要把和測試有關專案型別修改成.net8.0,前後只花了大概十分鐘,跑完測試後釋出了 FreeRedis 1.2.5

FreeRedis 是 .NETFramework 4.0 及以上 存取 redis-server 的使用者端元件

第二個要更新的開源專案是CSRedisCore,大致步驟同上,目前這個專案處於穩定維護階段,不再增加新功能。

CSRedisCore 是 .NETFramework 4.0 及以上 存取 redis-server 的使用者端元件,也是 FreeSql 作者早年釋出的 nuget 版本

第三個要更新的開源專案是FreeSql,這個專案比較龐大,解決方案內有50個子專案,由於主專案也是零依賴,所以基本不需要修改就支援.net8.0。最新編譯器提示.netcoreapp2.1高風險漏洞的警告,不得已移除了.netcoreapp2.1有關的依賴注入支援,前後大約花了半個小時,測試後釋出了 FreeSql 3.2.805

FreeSql 是一款功能強大的物件關係對映(O/RM)元件,支援 .NET Core 2.1+、.NET Framework 4.0+ 以及 Xamarin✨

第四個要更新的專案是FreeScheduler,這是一個純淨版的定時任務框架,依賴較少只花了5分鐘測試釋出。

FreeScheduler 實現輕量化定時任務排程,支援叢集、臨時的延時任務和重複迴圈任務(可持久化),可按秒,每天/每週/每月固定時間,自定義間隔執行,支援 .NET Core 2.1+、.NET Framework 4.0+ 執行環境。

其他幾個開源專案穩定且不依賴 .net 版本,所以本次無需維護更新。


測試與支援 FreeRedis aot

下午沒事去買了一杯咖啡,到12點鐘還睡不著,刷視訊刷到一點半還是睡不著,於是想折騰點什麼東西,正好.net 8.0 aot特性,測試一下FreeRedis,看看是否支援。

我是直接建立控制檯程式測試的,設定成aot釋出之後,.csproj 內容如下:

<Project Sdk="Microsoft.NET.Sdk">
	<PropertyGroup>
		<OutputType>Exe</OutputType>
		<TargetFramework>net8.0</TargetFramework>
		<PublishAot>True</PublishAot>
	</PropertyGroup>
	
	<ItemGroup>
		<PackageReference Include="FreeRedis" Version="1.2.5" />
	</ItemGroup>
</Project>

釋出 aot 需要使用控制檯命令:

dotnet publish -r win-x64 -c release

第一次釋出失敗,提示要安裝桌面版C++,於是我重新去官網下載vS2022企業版安裝器,執行它點選修改安裝,選中桌面版C++進行安裝,大概過了15分鐘,安裝完畢。

E:\github\FreeRedis\examples\console_net8>dotnet publish -r win-x64 -c release
適用於 .NET MSBuild 版本 17.8.3+195e7f5a3
  正在確定要還原的專案…
  所有專案均是最新的,無法還原。
  console_net8 -> E:\github\FreeRedis\examples\console_net8\bin\release\net8.0\win-x64\console_net8.dll
  Generating native code
C:\Users\28810\.nuget\packages\freeredis\1.2.5\lib\netstandard2.0\FreeRedis.dll : warning IL3053: Assembly 'FreeRedis'
produced AOT analysis warnings. [E:\github\FreeRedis\examples\console_net8\console_net8.csproj]
C:\Users\28810\.nuget\packages\freeredis\1.2.5\lib\netstandard2.0\FreeRedis.dll : warning IL2104: Assembly 'FreeRedis'
produced trim warnings. For more information see https://aka.ms/dotnet-illink/libraries [E:\github\FreeRedis\examples\c
onsole_net8\console_net8.csproj]
  console_net8 -> E:\github\FreeRedis\examples\console_net8\bin\release\net8.0\win-x64\publish\

只要編譯成功,釋出aot必然會成功,只是會有一些警告。

2023/11/16  13:06         5,637,120 console_net8.exe
2023/11/16  13:06       137,695,232 console_net8.pdb
2023/11/16  04:13           127,268 FreeRedis.pdb

請無視 .pdb 檔案,它是偵錯用途的可以刪除,console_net8.exe 只有 5兆大小。

第二次釋出後,執行成功了,純字串數值之內的操作全部成功。

正當得意之時,redis.AclGetUser 方法丟擲了一個新的錯誤,該方法返回的是一個實體型別 AclGetUserResult,有使用 Activetor.CreateInstance(typeof(AclGetUserResult)),Aot本身是支援這個方法的,錯誤提示是不支援該方法的物件型別 AclUserResult。

Unhandled Exception: System.MissingMethodException: No parameterless constructor defined for type 'FreeRedis.AclGetUserResult'.
   at System.ActivatorImplementation.CreateInstance(Type, Boolean) + 0x119
   at FreeRedis.RespHelper.CreateInstanceGetDefaultValue(Type) + 0x120
   at FreeRedis.RespHelper.MapToClass[T](Object[], Encoding) + 0x4a
   at FreeRedis.RedisClient.<>c__DisplayClass457_0.<AclGetUser>b__1(Object[] a, Boolean _) + 0x220
   at FreeRedis.RedisResult.ThrowOrValue[TValue](Func`3) + 0x58
   at FreeRedis.RedisClient.PoolingAdapter.<>c__DisplayClass9_0`1.<AdapterCall>b__0() + 0x141
   at FreeRedis.RedisClient.LogCallCtrl[T](CommandPacket cmd, Func`1 func, Boolean aopBefore, Boolean aopAfter) + 0x3bb
   at FreeRedis.RedisClient.LogCall[T](CommandPacket cmd, Func`1 func) + 0x63
   at FreeRedis.RedisClient.PoolingAdapter.AdapterCall[TValue](CommandPacket, Func`2) + 0x9a
   at console_net8.Program.Main(String[] args) + 0xa4
   at console_net8!<BaseAddress>+0x2c67f0

於是我係統的去看了官方aot檔案,發現檔案太過於簡陋,反覆看了七八遍也沒有找到相關的解決內容。不得已擴大了搜尋範圍,在谷歌搜尋鍵碼 .net aot 花了近一個小時,最終定位的關鍵字是 rd.xml,設定相當簡單,只需要把FreeRedis的型別全部設定即可。

對應的 .csproj 內容如下:

<Project Sdk="Microsoft.NET.Sdk">
	<PropertyGroup>
		<OutputType>Exe</OutputType>
		<TargetFramework>net8.0</TargetFramework>
		<PublishAot>True</PublishAot>
	</PropertyGroup>

	<ItemGroup>
		<RdXmlFile Include="rd.xml" />
	</ItemGroup>

	<ItemGroup>
		<PackageReference Include="FreeRedis" Version="1.2.5" />
	</ItemGroup>
</Project>

對應的 rd.xml 內容如下:

<Directives>
	<Application>
		<Assembly Name="FreeRedis"  Dynamic="Required All">
		</Assembly>
	</Application>
</Directives>

重新發布後,完美的解決所有問題,.exe 檔案體積增漲到 7兆。

這多虧當初設計FreeRedis的時候把依賴簡單最低,才這麼容易支援更多的執行平臺。


第二輪aot試驗 FreeScheduler

FreeRedis對.net 8.0以及aot的支援完美收官,這個時候已經凌晨三點,咖啡的勁還很足。

我本身對FreeSql Aot是不抱希望的,所以,就去測試FreeScheduler了。

FreeScheduler支援三種儲存方式,記憶體/資料庫/redis

基於記憶體,毫無壓力,直接通過測試。(得益於依賴較少)

基於redis,由於FreeRdis通過了aot測試,基本不會有太大的問題,記得設定好rd.xml,順利通過。

對應的 .csproj 如下:

<Project Sdk="Microsoft.NET.Sdk.Web">
	<PropertyGroup>
		<TargetFramework>net8.0</TargetFramework>
		<Nullable>enable</Nullable>
		<ImplicitUsings>enable</ImplicitUsings>
		<InvariantGlobalization>true</InvariantGlobalization>
		<PublishAot>true</PublishAot>
	</PropertyGroup>

	<ItemGroup>
		<RdXmlFile Include="rd.xml" />
	</ItemGroup>

	<ItemGroup>
		<ProjectReference Include="..\..\FreeScheduler\FreeScheduler.csproj" />
	</ItemGroup>
</Project>

對應的 rd.xml 內容如下:

<Directives>
	<Application>
		<Assembly Name="FreeScheduler"  Dynamic="Required All">
		</Assembly>
		<Assembly Name="FreeRedis"  Dynamic="Required All">
		</Assembly>
	</Application>
</Directives>

FreeScheduler還有一個web管理面板功能,抱著嘗試的態度試一試,建立.net8.0自帶的web API aot專案,把有關程式碼加到專案的執行,居然能直接通過,太牛逼了,這是我沒有想到的。

對應 Program.cs

using FreeRedis;
using FreeScheduler;
using Newtonsoft.Json;

var redis = new RedisClient("127.0.0.1,poolsize=10,exitAutoDisposePool=false");
redis.Serialize = obj => JsonConvert.SerializeObject(obj);
redis.Deserialize = (json, type) => JsonConvert.DeserializeObject(json, type);
redis.Notice += (s, e) =>
{
    if (e.Exception != null)
        Console.WriteLine(e.Log);
};
Scheduler scheduler = new FreeSchedulerBuilder()
    .OnExecuting(task =>
    {
        Console.WriteLine($"[{DateTime.Now.ToString("HH:mm:ss.fff")}] {task.Topic} 被執行");
        task.Remark("log..");
    })
    .UseStorage(redis)
    .Build();
if (Datafeed.GetPage(scheduler, null, null, null, null).Total == 0)
{
    scheduler.AddTask("[系統預留]清理任務資料", "86400", -1, 3600);
    scheduler.AddTaskRunOnWeek("(週一)武林大會", "json", -1, "1:12:00:00");
    scheduler.AddTaskRunOnWeek("(週日)親子活動", "json", -1, "0:00:00:00");
    scheduler.AddTaskRunOnWeek("(週六)社交活動", "json", -1, "6:00:00:00");
    scheduler.AddTaskRunOnMonth("月尾最後一天", "json", -1, "-1:16:00:00");
    scheduler.AddTaskRunOnMonth("月初第一天", "json", -1, "1:00:00:00");
    scheduler.AddTask("定時20秒", "json", 10, 20);
    scheduler.AddTask("測試任務1", "json", new[] { 10, 30, 60, 100, 150, 200 });
}

var builder = WebApplication.CreateSlimBuilder(args);

builder.Services.AddSingleton(scheduler);

var app = builder.Build();
var applicationLifeTime = app.Services.GetService<IHostApplicationLifetime>();
applicationLifeTime.ApplicationStopping.Register(() =>
{
    scheduler.Dispose();
    redis.Dispose();
});
app.UseFreeSchedulerUI("/freescheduler/");

app.Run();

2023/11/16  04:23               127 appsettings.Development.json
2023/11/16  04:23               151 appsettings.json
2023/11/16  13:34        25,104,384 Examples_FreeScheduler_Net80_aot.exe
2023/11/16  13:34       238,948,352 Examples_FreeScheduler_Net80_aot.pdb
2023/11/16  13:31            31,208 FreeScheduler.pdb

Examples_FreeScheduler_Net80_aot.exe 25兆,流弊了,雙擊執行它吧~~~~

開啟瀏覽器存取:http://localhost:5000/freescheduler/

意想不到,連管理面板都支援 AOT,這讓我有了繼續試驗的動力~~~


aot試驗意外收穫 FreeSql

四點了,還沒犯困!

最後抱著必涼的心態嘗試終極試驗,FreeScheduler使用資料庫持久化。

第一次失敗,報錯在FreeSql內部,這是是有TaskInterval型別不存在,它其實是FreeScheduler程式集的,並且rd.xml已經設定好了,反覆折騰仍然報錯。

Unhandled Exception: System.NotSupportedException: 'FreeScheduler.TaskInterval[]' is missing native code or metadata. This can happen for code that is not compatible with trimming or AOT. Inspect and fix trimming and AOT related warnings that were generated when the app was published. For more information see https://aka.ms/nativeaot-compatibility
   at System.Reflection.Runtime.General.TypeUnifier.WithVerifiedTypeHandle(RuntimeArrayTypeInfo, RuntimeTypeInfo) + 0x54
   at System.Array.InternalCreate(RuntimeType elementType, Int32 rank, Int32* pLengths, Int32* pLowerBounds) + 0x64
   at System.Array.CreateInstance(Type elementType, Int32 length) + 0x46
   at System.RuntimeType.GetEnumValues() + 0x53
   at FreeSql.Internal.Utils.GetTableByEntity(Type entity, CommonUtils common) + 0x138a
   at FreeSql.Internal.CommonProvider.CodeFirstProvider.<SyncStructure>b__51_0(CodeFirstProvider.TypeAndName a) + 0x6e
   at System.Linq.Enumerable.WhereSelectArrayIterator`2.MoveNext() + 0x3f
   at System.Linq.Enumerable.WhereEnumerableIterator`1.ToArray() + 0x5c
   at FreeSql.Internal.CommonProvider.CodeFirstProvider.SyncStructure(CodeFirstProvider.TypeAndName[] objects) + 0xd6
   at FreeSql.Internal.CommonProvider.CodeFirstProvider.SyncStructure[TEntity]() + 0x6c
   at FreeScheduler.TaskHandlers.FreeSqlHandler..ctor(IFreeSql fsql) + 0xf0
   at FreeSchedulerBuilder.Build() + 0x2f
   at Program.<Main>$(String[] args) + 0x180
   at Examples_FreeScheduler_Net80_aot!<BaseAddress>+0xca7fc3

從堆疊 GetEnumValues 可以看出是執行 Enum.GetValues 報錯,通過不斷嘗試中的其中一次,在程式開始寫了一行:

Console.WriteLine(Enum.GetValues(typeof(TaskInterval)));

重新發布後,又出現另一個錯誤,大致與上面的相同,只是型別變成了 TaskStatus,同樣加上一行程式碼:

Console.WriteLine(Enum.GetValues(typeof(TaskStatus)));

又出現了另一個錯誤:

Unhandled Exception: System.InvalidOperationException: The binary operator Equal is not defined for the types 'System.Reflection.Runtime.TypeInfos.NativeFormat.NativeFormatRuntimeNamedTypeInfo' and 'System.Reflection.Runtime.TypeInfos.RuntimeConstructedGenericTypeInfo'.
   at System.Linq.Expressions.Expression.GetEqualityComparisonOperator(ExpressionType, String, Expression, Expression, Boolean) + 0x26b
   at System.Linq.Expressions.Expression.Equal(Expression, Expression, Boolean, MethodInfo) + 0x63
   at FreeSql.Internal.Utils.<GetDataReaderValueBlockExpression>g__LocalFuncGetExpression|65_0(Boolean ignoreArray, Utils.<>c__DisplayClass65_0&) + 0x3916
   at FreeSql.Internal.Utils.GetDataReaderValueBlockExpression(Type type, Expression value) + 0x18d
   at FreeSql.Internal.Utils.<>c__DisplayClass66_0.<GetDataReaderValue>b__1(Type valueType2) + 0x68
   at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey, Func`2) + 0xa4
   at FreeSql.Internal.Utils.GetDataReaderValue(Type type, Object value) + 0x147
   at FreeSql.Internal.Utils.GetTableByEntity(Type entity, CommonUtils common) + 0x145c
   at FreeSql.Internal.CommonProvider.CodeFirstProvider.<SyncStructure>b__51_0(CodeFirstProvider.TypeAndName a) + 0x6e
   at System.Linq.Enumerable.WhereSelectArrayIterator`2.MoveNext() + 0x3f
   at System.Linq.Enumerable.WhereEnumerableIterator`1.ToArray() + 0x5c
   at FreeSql.Internal.CommonProvider.CodeFirstProvider.SyncStructure(CodeFirstProvider.TypeAndName[] objects) + 0xd6
   at FreeSql.Internal.CommonProvider.CodeFirstProvider.SyncStructure[TEntity]() + 0x6c
   at FreeScheduler.TaskHandlers.FreeSqlHandler..ctor(IFreeSql fsql) + 0xf0
   at FreeSchedulerBuilder.Build() + 0x2f
   at Program.<Main>$(String[] args) + 0x1a8
   at Examples_FreeScheduler_Net80_aot!<BaseAddress>+0xca8423

這次使用 vs2022 附加程序的方式進行了偵錯,深入 FreeSql 內部原始碼(表示式樹)環境,定位到了一行程式碼:

Expression.Equal(type, Expression.Contrast(Guid?))

把 Guid? 後面的問題去掉後,再次釋出。(由於每次釋出時間要20-30秒,重試時間成本太高,卡在這個問題已經有半個小時)

2023/11/16  04:23               127 appsettings.Development.json
2023/11/16  04:23               151 appsettings.json
2023/11/16  13:52        29,018,112 Examples_FreeScheduler_Net80_aot.exe
2023/11/16  13:52       238,948,352 Examples_FreeScheduler_Net80_aot.pdb
2023/11/16  13:31            31,208 FreeScheduler.pdb
2021/11/03  01:47         1,763,632 SQLite.Interop.dll

對於一個 web 專案並且包含 bootstrap 有關靜態資原始檔,.exe 檔案只有 29兆太滿意了

看到控制檯上的 SQL,太驚喜了,成功啦~~~~


最後建議

從 .net6.0 到 .net8.0,我們肉眼看不到變化,實際微軟做了很多內部工作,在 aot 使用體驗上明顯能感知。

有人說信創國產執行,那現在 aot 算什麼?

.net8.0 AOT 已經到了可用的階段,期待未來版本能改進以下問題:

  • 釋出速度變快,目前20-30秒一次實在太慢
  • 編譯前檢查錯誤,而不是等釋出後再報執行時錯誤
  • 加強偵錯,.pdb 100兆++ 為何偵錯還都是 c++ 有關內容,不能白瞎了這麼大的偵錯檔案啊
  • 儘快修復 Console.WriteLine(Enum.GetValues(typeof(TaskInterval))) 這個問題

我是開源人:https://github.com/2881099

Native AOT apps have the following limitations:

  • No dynamic loading, for example, Assembly.LoadFile.
  • No run-time code generation, for example, System.Reflection.Emit.
  • No C++/CLI.
  • Windows: No built-in COM.
  • Requires trimming, which has limitations.
  • Implies compilation into a single file, which has known incompatibilities.
  • Apps include required runtime libraries (just like self-contained apps, increasing their size as compared to framework-dependent apps).
  • System.Linq.Expressions always use their interpreted form, which is slower than run-time generated compiled code.
  • Not all the runtime libraries are fully annotated to be Native AOT compatible. That is, some warnings in the runtime libraries aren't actionable by end developers.