February Status Update

Maybe these are more monthly now than weekly, but it is Thursday, and thanks to the poor weather where I'm at, I've been staying at home a lot and working on Telodendria quite a bit. Here's the latest progress report on the code:

  • User Interactive Authentication: I'm working on properly implementing user interactive authentication. Previously, I'd just hacked together a dummy version of it for registration, but now it's time to do things right. The new Uia API will allow endpoints to build up the authentication flows they support, and then execute those flows. I've got most of the API in place, now I'm just finishing up the implementation details.
  • Account Management: I've built out the User API a bit. There's a few notable things that come with it:
    • Refresh Tokens: Refresh tokens can now actually be used to refresh access tokens. /_matrix/client/v3/refresh should be fully functional now.
    • Account Logout: Work has been done on the /_matrix/client/v3/logout endpoint. It should be functional, although we have yet to implement /_matrix/client/v3/logout/all.
  • It Works! Telodendria now has an "It Works!" page in the style of Synapse, primarily as a proof-of-concept for the other HTML pages we'll need (login/auth fallback pages) as well as the content repository.
  • Admin API: We've started thinking about what a Telodendria administrator API will look like. It's not very well defined at all, and no code has been written yet, but effort has been made on writing up some spec documents.

And of course, I can't forget about the project management side of things:

  • Documentation: The documentation is now mostly up to date. A lot of code is being written at the moment, so there's pending documentation to be written, but the documentation is a lot more up to date than it was the last time I wrote a newsletter.
  • Website: I hacked together a simple way to generate the index page based on the documentation available in the man/ folder. This way, I don't have to manually update the website every time I add a man page. I might change this later, but I think it's good enough for now.
  • Donations: Open source is a lot of work. I do find it personally rewarding, and I think that's very important, but Telodendria isn't going to build itself. It takes very real time and money to make things happen. I'm bringing this up again because I happened to stumble across core-js and read the tragic story of its owner. If you aren't donating time to a project you find interesting, consider donating money, at the very least to show the developers you appreciate what they're doing. This time I'm not necessarily pleading for you to donate to me in particular—though that's always appreciated—I just ask that you'd consider the idea of donating to open source in general if you care about it.

Memory

I have a lot to say about memory management in Telodendria, so I thought it best to give it its own section.

Backtraces

I spent probably a good 5 or so hours building up the Memory API so that it supports backtraces. I had it so that every time a call to a memory allocation/de-allocation function is made, we get the full call stack and store it in the block's metadata. The reason I wanted to do this is because it's getting harder and harder to debug memory issues, as the code is getting more and more complicated. So I thought that having bactraces in the log output whenever we have an invalid pointer or a memory leak would help significantly, because whenever I use Valgrind, I find I'm a lot more productive. My thinking is that the hook function could print the backtrace to the log whenever a memory error occurs, be it a leak or a bad pointer, so we know exactly where things went wrong.

The theory is sound, and I thought the implementation would be too, but alas, there is no POSIX-specified way of getting backtraces programmatically. I discovered that libexecinfo would do what I needed, especially because it ships with the modern BSDs, and the functionality it provides is built into Glibc. So I built out a beautiful backtrace API and even debugged a few memory issues with it. There was only one problem: Telodendria could no longer be built as a static binary; it had to be build dynamically so that the symbol table could be baked into it. If you've read the documentation, you know that goes against one of the project's goals, so that should have been the first indicator that I was going down a bad path. But it was a sacrifice I thought I was willing to make for pretty backtraces that could help me find memory issues.

Then I tried to compile on Alpine Linux, which famously does not use Glibc, but Musl libc. Of course it just so happens that Musl doesn't support backtraces. Apparently you can apk add libexecinfo libexecinfo-dev to get the backtrace functions, but I could not figure out how to link libexecinfo and Musl; apparently they are not compatible. I'm guessing libexecinfo is for those using Glibc on Alpine for whatever reason. So unfortunately, I had no choice but to scrap my backtrace idea because even though it's technically possible and in fact rather elegant, it's just not portable and I don't want to pull in a library like libunwind or libbacktrace just for something we shouldn't need to use much anyway.

That's my story about backtraces. The reason I bring this up is because it prompted everything that follows.

A Major Refactoring

After spending the last two days in Valgrind hunting down memory issues, I think it looks like Telodendria will have to undergo a major refactoring in the area of string management. A serious paradigm shift needs to take place. Currently, it's way too easy to make mistakes with strings, causing memory errors that are extremely difficult to track down, particularly when interacting with the JSON and the database APIs. As I work on this project myself, as well as debug the patches from other people, I'm finding that it is extremely difficult to write good, bug-free code in Telodendria.

Let me explain the problem: right now, we kind of just duplicate (or don't duplicate) strings on an as-needed basis when we need to pass them into a function we know is going to hang onto it for a while. What this means is that the caller of a function has to know whether or not that function is going to hang onto the string passed to it and then do exactly one of two things:

  • Duplicate the string and pass the duplicate into the function, hanging on to the original.
  • Just let the function "have" the pointer to the string and explicitly don't free it, because the caller knows it's still in use.

This is problematic because oftentimes, we actually have both of these things happening in the same function. That is, a string gets duplicated and passed into a function that will hang onto a pointer, but then later on, that same string is not duplicated and just straight up passed into another such function when the string is no longer needed anymore. The result is extremely ambiguous code; it's hard to tell if the duplication, or lack thereof, is intentional or just a programmer error. The only way to know is to comprehensively understand what all the functions are doing with their strings and where all the references to a string are at any given time in Telodendria's execution.

Believe it or not, there actually was a reason I chose to do things this way, and that was to avoid unnecessary string duplication. I was writing code with the goal in mind that Telodendria should be memory-efficient so it shouldn't be duplicating memory any more than it has to. Indeed, I still think this is the most efficient approach, as opposed to duplicating unconditionally and then freeing the original if necessary. But the problem is, the code is just too hard to understand and too hard to debug.

A part of simplicity is not necessarily efficiency, it's readability and write-ability. Obviously I want Telodendria to be efficient, but not at the cost of my own time and sanity. I've come to be okay with spending a few more bytes and clock cycles if it means the code is easier to maintain—that's why the Memory API exists in the first place, for example—because I'm going to be maintaining this thing for a long time. I want to be able to fully understand the code, including the drawbacks, so I think the code should be written in a way that is easy to understand, particularly because the code is getting extremely complex.

For that reason, I think I want to do string duplication unconditionally on the other end of the function call. That is, if a function is going to hang onto a string for a while by way of returning a reference that has a pointer to the string inside it somewhere, then that function itself should go ahead and duplicate it, so that the calling function does not have consequences for the caller. The caller should be able to operate on the string whether or not the function does something with the string. In this way, I want to make the code base a little more functional and less procedural; the difference being that functions cannot modify their input and produce new output, whereas procedures modify their input and produce no output.

For example, JsonValueString() should duplicate the strings passed into it instead of just generating a reference to that string. This alone would probably solve 75%-85% of our memory errors. Every caller of JsonValueString() just has to free any variables it passed into JsonValueString(), otherwise, if it itself was passed the string, it just lets its own caller free the string, and it can re-use those strings without worrying about references as needed. Basically what this does is allow us to keep track of strings a lot easier. Instead of having to think about where a string is referenced in other functions and objects, we can just get rid of it when we're done with it because anything that still needs it will have its own copy.

Another example is HashMap, which should do the same thing, since it takes string keys. This way we don't need the recently-addedHashMapGetKey() function, we can just internally manage our strings and free them when we delete them from the map. Literal strings are duplicated, but then they're freed when the map is, or when the key is removed. We don't need any logic to detect whether the string is on the heap or not, therefore reducing our dependency on the Memory API.

I'm still weighing the advantages and disadvantages of this approach. I think it comes at the cost of memory usage; our memory usage will go up in the case of literal strings (of which there are quite a few in Telodendria), as they'll be continuously duplicated on the heap in a few places, but at the same time, we're duplicating strings so much as it is because we can't have a single string reference in more than one place anyway. The other obvious downside is that this change is a huge refactor. Someone's going to have to go through and (1) add the appropriate StrDuplicate() calls and (2) track down all of the resulting memory leaks due to callers expecting strings to be taken care of by somebody else. That someone is probably going to be me, but even though it's a big time and effort commitment, I think it's worth it, because the code will becomeso much more readable and writable. Everyone that works on Telodendria will be less prone to errors because we don't have to mentally keep track of as much when we're writing code, and that alone should speed up future development by making the current contributors more efficient, and lowering the barrier of entry for potential contributors.

Thoughts? Concerns? Let me know about them over in #telodendria-general:bancino.net!

Previous Post Next Post