- Server, Nancy2/Marten/Hangfire, runs on linux
- Headless clients for linux and Windows (x64)
- GUI clients for macOS, linux and Windows (x64)
The server is deployed as a static fat binary. The headless clients are static binaries for linux and .NET proper for Windows.
The GUI clients use Eto, which in turn means:
- Static binary using Eto.Gtk on linux
- Static, signed app bundle (Gatekeeper friendly) using Eto.Xamarin on macOS
- .NET proper application using Eto.Wpf on Windows
No performance issues so far. I wish Mono would implement a GUI for the profiler though. The log profiler is great to use, but clunky in comparison with dotTrace or NProfiler.
The whole architecture sounds complicated but really isn't. It's a single solution with three* projects:
- Headless Clients
- Single Eto GUI for all platforms
- (* Target build projects for the GUI platforms, no code in here)
The build server then only needs to do some additional work for creating DMG installers and signing the macOS app.
Also, do your GUI clients have fancier UIs? For example, have you implemented your own grid view, something where you put custom controls in the cells, images, graphics, etc.?
Most of the user interaction is happening via the web app served by the server. The local clients are "just" for configuration, comparable in complexity with e.g. Tunnelbear or Glasswire. We use some custom grid elements, which have been easy to implement.
Eto is the only framework out there which gives you native GUIs everywhere. That's what was most important to us. Native speed, native OS integration (tray menus, notifications etc.). It's also really easy to learn for devs who might have been WPF-exclusive before.
Thank you for your detailed answer, by the way! :)
The UI is native: WPF on Windows, Objective C on Mac. The core synchronization logic is shared C# code. (Objective C and C# co-exist via Pinvokes and function pointers.)
We started with Mono in 2010 before a lot of the modern tools, libraries, ect. At the time mono was very buggy. Now, Mono is a lot more stable.
As far as insights go:
We originally had, on Mac, a C# UI built with a (now obsolete) framework called MonObjC. The problem with MonObjC was that the only way to understand it was to be an Objective C expert; and it didn't always wrap the APIs in ways we needed. Thus, when we had some lawyer come and tell us that they didn't like the license, we just rewrote everything to PInvoke into shim C functions that call into Objective C. Thus, IMO, when in doubt, PInvoke into native code instead of fighting a compatibility layer.
Our Mac app packaging is all homegrown, primarily because our application predates the more modern mono packaging. I haven't tried the newer app packaging that comes with Visual Studio on Mac. IMO, make sure you understand app packaging.
Quite frankly, we've always had trouble with live debugging. (This might be due to our older packaging scheme.)
Ultimately, you need to understand the cost of writing something twice. When you work with Mono, or any cross platform framework / language / ect, you won't "write once." Instead, you will write once, debug on all platforms, and then add a bunch of special cases. Then, the developers who work on platform X will make a change that breaks platform Y because it wasn't covered in a unit test. Sometimes the cost of writing something twice really is worth it compared to the tradeoffs. (This certainly is the case with our UI, where we want a 100% native UI.)
Edit: We use dependency injection. (Via code, not a framework.) We have a lot of base classes in common code, and then platform-specific subclasses to handle special cases.
I left that project two years ago but if starting something similar today I'd probably go with .net core. Not because of any issues with mono, but core just seems like the future. I didn't even realize that mono was still in active development, though I'm not really following that field anymore, so.
Just for the record, I never did notice any memory leaks that others complain about. Our servers stayed running for months at a time with no noticeable leaks.
While we have used mono only one „one“ platform technically (Unix), I would still like to add my own experience, since we encountered some different behaviour on macOS and Linux. Also I have spent the last week or so tracking down memory leaks for our App :)
First of all, tracking down memory leaks with mono is really not fun. The chances are very high, that the memory leak does not occur in the managed parts of either your or other people‘s code, else the garbage collector should have collected that anyways. While you do have an integrated memory profiler, it seems that they like to change them every so often and even the current one isn‘t very compatible. E.g. one user sent me a profiling report, that I could neither open via GUI nor via the commandline. Furthermore, the only GUI option available is Xamarin profiler, which does not run on Linux (more on that later). While it does the job, it likes to crash a lot and eats up a huge chunk of memory, especially if you have a large profiling snapshot.
Since I had to deal with unmanaged leaks (as indicated by the managed snapshots I collected), I tried to use valgrind. However, it seems that mono uses SIGSEGV instead of checking for null pointers beforhand and then uses signalhandlers to throw NullPointerExceptions. This would normally not be a problem, but one part of the mono System runtime is very likely to have a NullPointerException. Thus valgrind sees a SIGSEGV and mono‘s signal handler playing around with the stack. It decides that this is not acceptable and instantly termiantes the app. After I patched that out, valgrind seemed to work, except that it didn‘t find any leaks and the memory leak was not even there anymore!
So I just started randomly commenting out lines of functions that could potentially be involved and got lucky. I still haven‘t figured out why that line of code leaks memory though.
Furthemore, I also found some memory leaks inside mono‘s native HttpWebResponses.
We are still trying out figure out another memory leak that occurs and haven‘t had any luck finding out why. It‘s especially hard, since this particular memory leak only occurs on Linux. (If you want to see the full discussion I recommend you look at issues: https://github.com/Radarr/Radarr/issues/1580 and https://github.com/Radarr/Radarr/issues/3157).
On the other hand, from my experience the performance itself seems almost as good as on Windows, save for the memory leaks. Some big requests to the backend of the app might take 5-10% longer on mono, but that‘s negligble IMO.
So, in short, if you don‘t have any issues with performance it‘s good. However, as soon as you do have some issues, you will be faced with a lot of problems. There is no documentation on profiling or what could cause problems, except for a few standard things that‘s more generally .NET related. By pure luck, I found a guid that shows how you can do profiling for your mono app in Instruments, but that didn‘t help either, since the leaks only occur on Linux.
Nevertheless, we are planning on moving to .NET Core anyways, so no I would not use mono.
What are you going to switch to, btw?
That reminds me, the annual OpenSimulator Community Conference is this weekend .
It's a very mature project now, has been around more than a decade. I guess if it were started today, it would use .NET Core.
I have run it on Linux, and did not find any issues. Everything still worked.
Well, actually, it has/had a memory leak. Not sure if that's just on the Linux platform.
On Linux the UI is via the browser (the service has a web interface).
I think this is the only C# project I use currently.