Dart is awesome language which going to be multi-purpose like Python is. It can work on every platform without significant changes, it can use relatively-native components and interact with external libraries using “ffi”. Sounds too cool to be true maybe? I tried to check it last year and here is my experience, feelings and thoughts.
I planned to write a telegram-bot and a REST server for my pet project — a cooperative card game, a mix of RPG and story-telling. Cards are like random events and should be picked by the player. I thought to write client-agnostic server to bring a random card for the player.
Tools and libraries
As experienced PHP developer I expect a number of build-in functions from every REST-ready framework:
- Flexible routing system
- Native JSON support
- Configurable validation rules
- Mapping JSON to objects and vice-versa
Also, it is common thing for a backend developer to have nice build-in support of any relational SQL database for persistent data storage and any in-memory database for caching.
So, my first step was to find something from this list in Dart’s ecosystem. Here are results of my research.
For basic MySQL support I found Mysql1 package (https://pub.dev/packages/mysql1). Sadly, this library does not provide full MySQL support. If you habit to use powerful ORM solutions — you will be rather disappointed. With this library you will be able to write only raw SQL requests without any significant abstraction level. But I have met problems even with this basic functionality: the library has lack of data types support; in my case it was BLOB. Such data columns are simply ignored in SELECT requests.
Another problem of this library was speed and working asynchronically with rest of dart code. Many times I catch unexpected exceptions and waited too long while a simple request would be processed.
So, this MySQL library was (and is) generally not for production use. And there is not any perspective that situation would be changed soon: just look at the library’s changelog, where the distance between minor fixes is about a year!
I see that now there is a “successor” of mysql1 — mysql_client (https://pub.dev/packages/mysql_client). I did not test it and have not plans to, but look to it’s changelog: it also is… at deep alpha I suppose.
Of course, there are alternatives with Postgres, where you have even 2 actively maintained libraries: postgresql2 (https://pub.dev/packages/postgresql2) and postgres (https://pub.dev/packages/postgres).
Similar situation is with in-memory storage. In backend development I usually choose Redis because it is simple enough for any team member, it can automatically remove outdated data and it can save data on hard drive when there is not enough of memory.
It the moment of working under my application I have found Dartis library (https://pub.dev/packages/dartis). It worked perfectly, but looks like discontinued. And now it is approximately 2 years passed from last update!
Dartis also have alternative: Redis (https://pub.dev/packages/redis). It looks like being still alive but who dare to predict the future whereas so many libraries were partially done, become popular and successful and was abandoned after all?
Finally, I found a compromise solution: Parse server with it’s SDK (https://pub.dev/packages/parse_server_sdk). This is not a database itself, but it has SQL database underneath. Also, it is not an unique service hosted by any corporation, like Firebase is — I don’t like such solution because I want to be free to migrate to my own hardware.
So, despite of fact that I found necessary toolset, I think this case in not success: a very popular database for backend is barely supported yet, and you have no confidence with existing and actively maintained libraries: how long would it be alive? I wiped out any Redis support from my project to avoid an “legacy” dependency at future. It means that I need to save all runtime data at application’s variables, that mean possibility of partial data loss between “points” where data is saved into Parse server (which is slow operation to perform it too often). Well, I thing it would be disaster for real-life business application in production. Too sad that you have not an easy way and tool to solve this.
The second core part of server application is how it works with requests, parse incoming data and how fast and flexible newborn API could be.
Here I should notice that writing APIs today is much more important part for backend developer than integrating markup, writing parsers and so on. PHP is not Wordpress, right? So, looking at Dart I dreamed about some powerful and flexible tool like Laravel/Lumen/Symfony…
Firstly, I found Aqueduct. I even tried it — it works really fast on local machine, spawning multiple isolates for parallel requests handling. But again, a typical disappointment: the library is abandoned, the code even does not support null-safety.
I also googled several small libraries aimed to reproduce some features from more mature frameworks, but finally I put my eye on Shelf (https://pub.dev/packages/shelf) — it seems to be well-supported library and most probably it would not be abandoned at future. The second reason was that its library is used by https://datagrok.ai — a serious and complex service, written fully on Dart, both frontend and backend.
So, what are my impressions about shelf after PHP development? Its functionality is very minimal. You should care about everything: how to implement desired routes, how to read requests from raw data to JSON and then how to validate it and how to convert into objects… Some of those tasks could be solved by using external library (hopefully would not be abandoned at future), but I decided again not to rely on it.
In other hand, Dart allows to write short and clean code so I think I made nice solution and even enjoyed by the process. Here is the code: https://github.com/litgame/rest-server/tree/main/lib/service/api Validators, controllers, actions… I felt myself like I creating a fork of Yii2 on Dart. Should you every time do it in you work, should you invent a new unique framework for every yours’s project again and again? Definitely not. But with pure Dart’s ecosystem you need to.
I planned to create three clients for the game:
- Telegram bot
- Android application
- Web build of Android application
Because this article is focused on backend, I going to tell only about telegram bot.
There is awesome library: TeleDart (https://pub.dev/packages/teledart). It supports a lot of features and well-maintained. The library works either in long-polling mode and in web-hook mode. It also is friendly for inventors like me, who tries to create a complex framework to solve tasks gracefully.
The client is much more complex application than server: it should interact with user, build interfaces, send messages, validate input and so on. Also, it stores much more information about game’s state and should keep it in sync with server. Looking at server application, I was absolutely sure that there would not be any memory leak or slow operation. But I was not so sure about the bot application. So, I performed a little check.
Stress test: stability and memory consumption
I implemented the bot using “long-polling” approach. This means that it could not be actually “highload” because the bot takes a fixed number of messages from queue, it does not matter how much messages still pending. As result, the REST server would not be “highload” too. Additionally, if we talking about performance testing, we should to have something to compare with, but I’m not ready to implement all mechanics twice, on PHP or node.js…
That’s why we will focus only on memory consumption. Firstly, here are data from TICK monitoring on production server after two months of uptime:
As we can see, memory consumption stays in fixed bounds. The fall on top graph most probably was triggered by rebuilding and restarting the docker container of the bot. But in most causes telegram bot need only 60–70 megabytes of memory.
The “ladder” at bottom graph is rest-server’s data. It is much lighter and consumes less memory and here you can see how garbage collector (GC) works: GC does not attempt to clean the memory unless there were too high change of consumption.
To illustrate GC work more clearly, I created a fake telegram-server which started 100 games with 5 players simultaneously. Virtual players played a bit and then all games had been finished. My expectations were that GC should be started after finishing all games (when a lot of clear() functions are called)
Here is the graph from DevTools Memory Page:
Let’s see at selected position on timeline. In first implementation of stress-test all games had been finished at this position, but GC did not call, memory stayed full of dead data. Looking at this I decided to make “second pass”: to start new test just after previous. This led the application to allocate more memory, consumption become too high and GC was called.
It evidently illustrates that RAM would not be freed after some memory expensive operations. Service already would not be under load, but will consume resources. And only another “highload session” will trigger GC to free memory… to consume it again immediately.
Is it good or bad? Depends on point of view. For server-side applications working concurrently on resource-limited server it is not nice behavior. There is no any memory leak, of course. But resources are consumed whereas service do not need them really. But any other service at this machine could be starving for memory. You can’t to control memory consumption manually. You can’t call GC from production code. You can only hope that your machine has enough memory and there is no another service pretending on it.
GC behavior is not constant, it changes from time to time, may be depending on other processes launched on the machine. For example, I started test again and found memory graph a bit different from previous launch:
The picture is similar for REST server. But this application is much lighter itself and GC triggers very rarely. You can see again, how the application holds a lot of memory without using it. I waited a long time, if GC will come, but hopeless… After all, I triggered GC manually from DevTools and you can see how much memory become freed:
Here is memory snapshot before manual GC call:
Highlighted entities are not used already, but Dart still keeps it in memory. All these entities disappearing after manual GC triggering:
Two highlighted entities are not a memory leak. These were specially cached.
After all, here is my personal opinion about writing a server application on Dart:
- You will be limited by libraries, tools and framework functionality.
- You also should examine carefully every package you going to use. Because a lot of libraries are unsupported or provide very minimal functionality.
- Keep in mind that even very popular library might be discontinued. It is true for all opensource of cause, but especially for Dart ecosystem at back-end.
- Your application should be very-very simple. In other cause you’ll have to reinvent a lot of bicycles. Maybe you like to. But perhaps you time is worth more?
- GC is not fine for long-running server-side daemons.
- You should implement multi-threaded requests handling yourself. The Dart could be super-fast at single process but every production service could be super-loaded. In PHP world it usually solved by CGI manager. In Dart… the Aqueduct is abandoned and I did not find any other solution which would care about that. So, you might choose a very fast programming language and create a very slow application on it 😊
Of cause, the Dart is awesome technology, I really like it. But let’s be honest and objective. Every modern application does not consist of programming language only. Modern applications consist of ecosystem around programming language, and ecosystem is exactly what makes a language powerful and useful. With pure ecosystem even most powerful language become weak and produce problems and challenges. Sadly, this is exactly what’s happens with Dart on server now.