2024-03-16 23:06:13 +00:00
// ----------------------------------------------------------------------------------------------
2020-05-22 09:48:02 +00:00
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
2024-03-16 23:06:13 +00:00
// ----------------------------------------------------------------------------------------------
2024-03-17 01:35:40 +00:00
// |
2024-01-08 10:33:28 +00:00
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
2020-05-22 09:48:02 +00:00
// Contact: JustArchi@JustArchi.net
2024-03-17 01:35:40 +00:00
// |
2020-05-22 09:48:02 +00:00
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
2024-03-17 01:35:40 +00:00
// |
2020-05-22 09:48:02 +00:00
// http://www.apache.org/licenses/LICENSE-2.0
2024-03-17 01:35:40 +00:00
// |
2020-05-22 09:48:02 +00:00
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System ;
2024-02-21 02:09:36 +00:00
using System.ComponentModel.DataAnnotations ;
2020-05-22 09:48:02 +00:00
using System.Composition ;
using System.Runtime ;
2024-02-21 02:09:36 +00:00
using System.Text.Json.Serialization ;
2020-05-22 09:48:02 +00:00
using System.Threading ;
Plugins breaking: Convert all synchronous interface methods to Task
Okay, I wish we had uncovered it earlier as part of V5.2 but it has bitten us in the back just now, so I'm addressing it as part of monthly cycle instead.
Previously used void methods did not allow async operations in plugins in a "nice way". If plugin didn't require synchronization with the ASF and just minded its own business, it wasn't half bad as it could use async void signature. However, if plugin by any chance had to do something BEFORE ASF continued with the rest of the logic, it had to explicitly leave non-async void signature and call its async-capable stuff in synchronous manner (usually with Wait() or .Result), which is vastly suboptimal.
This was visible even in our STD plugin, which previously had (and still has) GlobalCache initialization in OnASFInit(). If that cache initialization took a bit longer time, STD would hit InvalidOperationException() in OnLicenseList() callback as global cache didn't load yet while we were already long past OnASFInit().
Therefore, I've decided to make a breaking change for a very good reason - all previous methods were converted to tasks, which allows from plugin to do one of three things:
- If plugin is async and requires synchronization (like STD), it can declare itself as async await, and do its awaits as-needed, and ASF will wait for those.
- If plugin is truly synchronous (and not just a synchronous signature with awful Wait() or .Result, see above), it can simply return Task.CompletedTask and has exactly the same logic.
- Finally, if plugin calls some async stuff but doesn't need ASF synchronization, it can "offload" itself from it by calling e.g. ASF's Utilities.InBackground() with whole logic, while returning Task.CompletedTask from the main method. This will allow it to effectively do what async void previously did, by just hooking into the process without intention of slowing it down.
All in all I'm confident this approach, while a bit counter-intuitive at first, will result in better compatibility between ASF and the plugins, as if I wanted to fix my STD issue right now without that breaking change, I'd have to actually call .Result on my async global cache loader function, which is utterly stupid if we can fix ASF to do the right thing instead.
This "approach" can be commonly found in some other libs with similar to ASF's event-hook behaviour, e.g. Discord.Net.
You'll sadly need to do some method signature changes in all of your plugins, as the core OnLoaded() was also changed. See the ones I did in SteamTokenDumperPlugin.cs if you need a practical example, and see ExamplePlugin.cs if you need further explanation.
2021-12-08 15:52:27 +00:00
using System.Threading.Tasks ;
2021-05-07 23:37:22 +00:00
using ArchiSteamFarm.Core ;
using ArchiSteamFarm.Plugins.Interfaces ;
2022-12-15 18:16:28 +00:00
using JetBrains.Annotations ;
2020-05-22 09:48:02 +00:00
2021-11-10 20:23:24 +00:00
namespace ArchiSteamFarm.CustomPlugins.PeriodicGC ;
2020-05-22 09:48:02 +00:00
2021-11-10 20:23:24 +00:00
[Export(typeof(IPlugin))]
2022-12-15 18:16:28 +00:00
[UsedImplicitly]
2021-11-10 20:23:24 +00:00
internal sealed class PeriodicGCPlugin : IPlugin {
private const byte GCPeriod = 60 ; // In seconds
2020-05-22 09:48:02 +00:00
2021-11-10 20:23:24 +00:00
private static readonly object LockObject = new ( ) ;
private static readonly Timer PeriodicGCTimer = new ( PerformGC ) ;
2020-05-22 09:48:02 +00:00
2024-02-21 02:09:36 +00:00
[JsonInclude]
[Required]
2021-11-10 20:23:24 +00:00
public string Name = > nameof ( PeriodicGCPlugin ) ;
2020-05-22 09:48:02 +00:00
2024-02-21 02:09:36 +00:00
[JsonInclude]
[Required]
2021-11-10 20:23:24 +00:00
public Version Version = > typeof ( PeriodicGCPlugin ) . Assembly . GetName ( ) . Version ? ? throw new InvalidOperationException ( nameof ( Version ) ) ;
2020-05-22 09:48:02 +00:00
Plugins breaking: Convert all synchronous interface methods to Task
Okay, I wish we had uncovered it earlier as part of V5.2 but it has bitten us in the back just now, so I'm addressing it as part of monthly cycle instead.
Previously used void methods did not allow async operations in plugins in a "nice way". If plugin didn't require synchronization with the ASF and just minded its own business, it wasn't half bad as it could use async void signature. However, if plugin by any chance had to do something BEFORE ASF continued with the rest of the logic, it had to explicitly leave non-async void signature and call its async-capable stuff in synchronous manner (usually with Wait() or .Result), which is vastly suboptimal.
This was visible even in our STD plugin, which previously had (and still has) GlobalCache initialization in OnASFInit(). If that cache initialization took a bit longer time, STD would hit InvalidOperationException() in OnLicenseList() callback as global cache didn't load yet while we were already long past OnASFInit().
Therefore, I've decided to make a breaking change for a very good reason - all previous methods were converted to tasks, which allows from plugin to do one of three things:
- If plugin is async and requires synchronization (like STD), it can declare itself as async await, and do its awaits as-needed, and ASF will wait for those.
- If plugin is truly synchronous (and not just a synchronous signature with awful Wait() or .Result, see above), it can simply return Task.CompletedTask and has exactly the same logic.
- Finally, if plugin calls some async stuff but doesn't need ASF synchronization, it can "offload" itself from it by calling e.g. ASF's Utilities.InBackground() with whole logic, while returning Task.CompletedTask from the main method. This will allow it to effectively do what async void previously did, by just hooking into the process without intention of slowing it down.
All in all I'm confident this approach, while a bit counter-intuitive at first, will result in better compatibility between ASF and the plugins, as if I wanted to fix my STD issue right now without that breaking change, I'd have to actually call .Result on my async global cache loader function, which is utterly stupid if we can fix ASF to do the right thing instead.
This "approach" can be commonly found in some other libs with similar to ASF's event-hook behaviour, e.g. Discord.Net.
You'll sadly need to do some method signature changes in all of your plugins, as the core OnLoaded() was also changed. See the ones I did in SteamTokenDumperPlugin.cs if you need a practical example, and see ExamplePlugin.cs if you need further explanation.
2021-12-08 15:52:27 +00:00
public Task OnLoaded ( ) {
2021-11-10 20:23:24 +00:00
TimeSpan timeSpan = TimeSpan . FromSeconds ( GCPeriod ) ;
2020-05-22 09:48:02 +00:00
2021-11-10 20:23:24 +00:00
ASF . ArchiLogger . LogGenericWarning ( $"Periodic GC will occur every {timeSpan.ToHumanReadable()}. Please keep in mind that this plugin should be used for debugging tests only." ) ;
2020-05-22 09:48:02 +00:00
2021-11-10 20:23:24 +00:00
lock ( LockObject ) {
PeriodicGCTimer . Change ( timeSpan , timeSpan ) ;
}
Plugins breaking: Convert all synchronous interface methods to Task
Okay, I wish we had uncovered it earlier as part of V5.2 but it has bitten us in the back just now, so I'm addressing it as part of monthly cycle instead.
Previously used void methods did not allow async operations in plugins in a "nice way". If plugin didn't require synchronization with the ASF and just minded its own business, it wasn't half bad as it could use async void signature. However, if plugin by any chance had to do something BEFORE ASF continued with the rest of the logic, it had to explicitly leave non-async void signature and call its async-capable stuff in synchronous manner (usually with Wait() or .Result), which is vastly suboptimal.
This was visible even in our STD plugin, which previously had (and still has) GlobalCache initialization in OnASFInit(). If that cache initialization took a bit longer time, STD would hit InvalidOperationException() in OnLicenseList() callback as global cache didn't load yet while we were already long past OnASFInit().
Therefore, I've decided to make a breaking change for a very good reason - all previous methods were converted to tasks, which allows from plugin to do one of three things:
- If plugin is async and requires synchronization (like STD), it can declare itself as async await, and do its awaits as-needed, and ASF will wait for those.
- If plugin is truly synchronous (and not just a synchronous signature with awful Wait() or .Result, see above), it can simply return Task.CompletedTask and has exactly the same logic.
- Finally, if plugin calls some async stuff but doesn't need ASF synchronization, it can "offload" itself from it by calling e.g. ASF's Utilities.InBackground() with whole logic, while returning Task.CompletedTask from the main method. This will allow it to effectively do what async void previously did, by just hooking into the process without intention of slowing it down.
All in all I'm confident this approach, while a bit counter-intuitive at first, will result in better compatibility between ASF and the plugins, as if I wanted to fix my STD issue right now without that breaking change, I'd have to actually call .Result on my async global cache loader function, which is utterly stupid if we can fix ASF to do the right thing instead.
This "approach" can be commonly found in some other libs with similar to ASF's event-hook behaviour, e.g. Discord.Net.
You'll sadly need to do some method signature changes in all of your plugins, as the core OnLoaded() was also changed. See the ones I did in SteamTokenDumperPlugin.cs if you need a practical example, and see ExamplePlugin.cs if you need further explanation.
2021-12-08 15:52:27 +00:00
return Task . CompletedTask ;
2021-11-10 20:23:24 +00:00
}
2020-05-22 09:48:02 +00:00
2021-11-10 20:23:24 +00:00
private static void PerformGC ( object? state = null ) {
ASF . ArchiLogger . LogGenericWarning ( $"Performing GC, current memory: {GC.GetTotalMemory(false) / 1024} KB." ) ;
2020-05-22 09:48:02 +00:00
2021-11-10 20:23:24 +00:00
lock ( LockObject ) {
GCSettings . LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode . CompactOnce ;
GC . Collect ( GC . MaxGeneration , GCCollectionMode . Forced , true , true ) ;
2020-05-22 09:48:02 +00:00
}
2021-11-10 20:23:24 +00:00
ASF . ArchiLogger . LogGenericWarning ( $"GC finished, current memory: {GC.GetTotalMemory(false) / 1024} KB." ) ;
2020-05-22 09:48:02 +00:00
}
}