-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Design Proposal: Reducing the reflection dependency #1
Comments
Hey @matt-andrews, thanks for your interest and taking the time to write the proposal !
It really depends, most apps will do I/O with the database which really is the biggest culprit of performance instead of reflection. The source generator is an interesting idea that I'm willing to explore to improve things up but as mentioned in your I'm also interested in how that would work in the next iteration of the IPC. I'd like to share some progress I've made behind the scenes. I've reworked the IPC, such that it allows more creative freedom. The current solution involves only method invokation without any level of depth. For example I can't do This is especially problem when it comes to getting something from a property. Since you can't get properties you'd have to write a method to get that property which introduces more boilerplate. I've devised a new way of interacting with IPC that allows you to do this. Technically how it works is by creating this JSON: {
"uuid": "...",
"instance": "calculator",
"path": "instanceOfClass.propertyOfClass.anotherPropertyOfClass.InvokeMethod($1);",
"args": {
"$1": [{ name: "Data" }]
}
} (I'm not really entirely sure why it's highlighting part of it in red) private static async Task<object?> Execute(object instance, string path,
Dictionary<string, List<JsonElement>>? args)
{
var parts = path.Split('.');
var currentObject = instance;
foreach (var part in parts)
{
currentObject = part.Contains('(')
? await HandleMethodInvocationAsync(currentObject, part, args)
: HandlePropertyAccess(currentObject, part);
}
return currentObject;
}
// I'm only including the method invocation part. I've also removed error checking, to reduce the code for brevity reasons
private static async Task<object?> HandleMethodInvocationAsync(object? currentObject, string part,
Dictionary<string, List<JsonElement>>? args)
{
var openParenIndex = part.IndexOf('(');
var closedParenIndex = part.IndexOf(')');
var methodName = part[..openParenIndex];
var type = currentObject.GetType();
var method = type.GetMethod(methodName);
if (!IsIpcExposed(method) && !IsIpcExposed(currentObject.GetType()))
throw new InvalidOperationException($"Method '{methodName}' is not exposed to IPC.");
var parameters = method.GetParameters();
var argKey = part[(openParenIndex + 1)..closedParenIndex];
var methodArgs = args != null && args.TryGetValue(argKey, out var jsonElements)
? ResolveArgumentsFromJson(parameters, jsonElements)
: null;
// InvokeMethodAsync, is a helper that will either invoke sync or invoke and await async functions
return await InvokeMethodAsync(currentObject, method, methodArgs);
} I wonder in that case if the generated code would work... In a first glance it seems like it would work perfectly, but I thought of sharing my WIP anyways since it's different than what we have now in the library. But I think it's better in terms of IPC code since now you can do something like: public class Playlist
{
public List<Song> Songs { get; set; } = [];
public async Task AddSongToDatabase(Song song)
{
// simulate database call
await Task.Delay(1000);
Songs.Add(song);
Console.WriteLine($"Adding {song.Name} to database");
}
}
public class Song(string name)
{
public string Name { get; set; } = name;
public void Play()
{
Console.WriteLine($"Playing {Name}");
}
}
// inside the container:
Register("playlist", new Playlist()); And when it comes to javascript, it allows you to do: await ipc().playlist.AddSongToDatabase({ Name: "s1" }).send();
await ipc().playlist.Songs[0].Play().send(); |
Hello stranger! I ran across your reddit post for this project, to be completely honest I don't have a lot of use for desktop applications these days, but I've identified a potential solution to your reflection problem and thought I'd share (if you want). This might be considered a micro-optimization, so feel free to tell me to go to hell.
Summary
Reflection is expensive. In an application such as this, efficient interaction between the framework and the users code is critical for widespread adoption. I will spend the following wall of text explaining how we can accomplish this.
Solution
Source Generators! tl;dr source generators are a roslyn analyzer feature that when implemented, generates code based on code the user enters. We can then use the generated code to interface with user code instead of relying on reflection.
The following rough examples are in the context of registering a component and invoking the methods of a component.
Imagine a new interface:
And imagine that your
IpcContainer
looks more like this:Now the changes above are already a huge performance boost in regards to registering a new component - since all were doing is managing a dictionary. So how do we make this work with the rest of the framework, you might ask? This is where source generation comes in. Imagine a simple component:
Using a simple source generator we can very easily generate the following, triggered by the attribute:
With the generated partial class, the
TestComponent
class is now of typeIIpcComponent
and can be registered with theIpcContainer
, leading us to the ability to invoke methods dynamically without the cost of reflection.Note
I'm not including the actual source generation code here because it would bloat this proposal even more. Suffice to say the above code is 100% generated lol
Benchmarks
These benchmarks probably aren't super accurate since I threw them together on short notice, but the numbers are interesting to consider:
Component Registration
BenchmarkDotNet v0.14.0, Windows 11 (10.0.22621.1702/22H2/2022Update/SunValley2)
12th Gen Intel Core i5-12600K, 1 CPU, 16 logical and 10 physical cores
.NET SDK 8.0.400
[Host] : .NET 8.0.8 (8.0.824.36612), X64 RyuJIT AVX2
DefaultJob : .NET 8.0.8 (8.0.824.36612), X64 RyuJIT AVX2
These metrics are not surprising at all, since the V2 version of the
IpcContainer
is basically just a wrapper over a dictionary - but it shows what kind of penalty we're paying with the original implementation.Component Invocation
These numbers are a little more interesting, this is calculated by getting a component from the container and invoking it with
_container.Invoke("Add", [10, 5]);
and compared to a slimmed down version of the original. As the metrics show, we can save a significant amount of time on the invocation by using source generators instead of reflection.Cons
So we've talked about all the pros for source generators, what are some of the cons?
GetType().GetMethods()
and know that reflection is being used to get a types methods; but magic attributes that make a type suddenly have an interface and a method that gets other methods (???)IpcExposeAttribute
or have methods that useIpcExposeAttribute
must bepartial
, this would be a breaking change and require a major version bump :(Conclusion
Thinking back I'd probably use an abstract class instead of an interface for
IIpcComponent
- it kind of feels more natural, and frees us up to pass in cool things during the component lifecycle if we wanted in the future.I don't really have much else to add tbh, I couldn't sleep last night so this is what I did instead lol. Anyways, this was a fun thought experiment since I've never actually created a source generator before today.
If I don't hear from you again - good luck!
The text was updated successfully, but these errors were encountered: