Testing Keyboard Input Latency

I occasionally see people go through great effort to do end-to-end testing of keyboard input latency. That is fantastic but it requires hardware and patience I don’t, nor will ever, have.

Here is a much simpler way to get about 90% of the value. For example, everything but driver/interrupt handler latency and display link scanout/monitor visibility latency and of course your app side (but you could theoretically rig this up to do that too, inside your app). Not that those aren’t important, but they definitely fall into the category of things I personally cannot control for you.

Keyspeed is a very simple GTK application which uses /dev/uinput to synthesize keypresses. Since it knows the time of provenance, it can compare that to when it gets the event back from compositor delivery.

Wrap all that data up in Sysprof capture marks, pull in some from the compositor (GNOME Shell/Mutter support this), tie in some callgraphs/flamegraphs, and you have a very good overview of what is going on during your keypress.

Run it like this (but remember to chmod back when you’re done less you have attack surface available).

$ sudo chmod 660 /dev/uinput
$ git clone https://gitlab.gnome.org/chergert/keypress
$ sudo dnf install sysprof-devel libinput-devel gtk4-devel
$ make
$ sysprof-cli --gtk --gnome-shell capture.syscap -- ./keyspeed
$ sysprof capture.syscap

a screenshot of keypress. key being press/release on left, time it took on the right.

Currently, this only shows you keypress send to receive in GTK, but if someone cared enough, you could make it take the next GtkFrameTimings and use that to get the presentation time. I don’t need that for what I’m doing, so it doesn’t.

If you go to the marks section, you can dive in to a specific keypress/release cycle. Zoom in on just that section, switch back to callgraph/flamegraph profiler and see what was going on.

Pretty simple, no special hardware needed.

You can see how long it took, where time was spent, and more importantly, how much time was empty between things that matter.

A screenshot of sysprof showing the marks section with timing information for minutia happening across GTK/Mutter for full event delivery timing.

A screenshot of sysprof showing the marks section with timing information for minutia happening across GTK/Mutter for full event delivery timing.

A Data Layer for GTK applications

Gom is a very old object mapper I wrote to bridge GObject to SQLite. It made a lot of assumptions about the world based on when it was prototyped.

The past couple years had me using it again for the documentation search in Manuals. Typically, I would have just built Manuals to parse all the XML files on disk and hold them in memory. That’s how both Devhelp and Builder always did things. Once we started supporting Flatpak SDKs that was no longer realistic. You could have numerous SDKs all with copies of the overlapping data and it just became easier to have a query model.

One of the more performance critical limitations was the locking model. When gom-1.0 was written, it was not common for distributions to compile SQLite with locking support. So you just created a single thread and did your work over there.

Bolting fulltext search and many other missing features onto the old ABI just wasn’t realistic. Especially when I’ve wanted to make the thing properly async for years. One of my other projects, Libdex is just right over there and perfect for this sort of problem.

The landscape changed and so do our horizons.

A new informed ABI

In the years after Gom was prototyped, I worked at a commercial database company and learned a great deal about implementing the internals of both that database and more traditional RDBMS. That left a certain cringe on my mouth whenever looking at my code predating it. Knowing how things get done inside the database allows for building better APIs to interact with it.

This time everything is async. Queries are modeled like you do with a compiler. Lowered into the back-end specific implementation. There can be an entity map and real transactions which allows you to read back the same instance despite which query inflated it.

The Center

Your early stage objects are the GomRepository, GomDriver, and GomRegistry.

The registry describes the entities that can exist within the repository. This is handy because it allows us to pre-compile information into a model that is both immutable and fast at runtime. Compare that to methods like g_object_class_list_properties() which is a performance bottleneck of its own.

The driver is very obvious. It is our abstraction layer for the database engines. Currently we have support for SQLite and PostgreSQL.

The repository is the center of the center. It is how you query, insert, update, delete, transact, and more. It is likely your application instance owns one of these unless of course you use Gom as your file format in which case you’ll have one per “document”.

Two Access Models

This new version of Gom can support either the entity mapping you’re used to or; optionally, raw access to relations/projections via the GomCursor.

As the cursor moves through the resulting rows you will have access to all the projections requested in the query. Though it holds enough information to allow you to gom_cursor_materialize() the row into an GomEntity subclass.

If you want a snapshot of that cursor row without materializing, you can use GomRecord which can also conveniently be used in GListModel for integration into GTK applications.

Most of the time, you’ll use materialization. And even then it is likely to happen through automated collections rather than with a cursor directly. More on that later.

Sessions

As I mentioned, there was no concept of transactions previously.

In this iteration we have GomSession. It is your standard identity-map layer with transaction-scoping. If you perform multiple queries for the same record, the session will ensure you get the same instance back. That is essential when you do local mutations on an instance and what to see that reflected in followup queries.

Additionally, it makes it nice to have multiple views of an object with an editor or listview and needing them to stay in sync.

Relationship Modeling

Support for relationships was adhoc previously. We had some functions named in ways that made you think you could, but I assure you, they were not well tested.

This time around you can model your GomEntity with 1:1, 1:M, M:M, inverse, self-referencing, all while handling proper delete rules. Combing this with the session support mentioned previously is crucial.

So now you should be able to show related models easily in GtkListView while keeping the paginated-and-lazy model beneath it transactional.

Migrations

In the previous version migrations were dynamic, but largely controlled by Gom itself. Very inflexible.

This time around we have things broken down into Migrator and Migration.

You can use built-in implementations like the EntityMigrator or implement your own. CustomMigrator makes that easy. Especially since you can inject your own migrations at just the right point.

Internally, libgom-2 can snapshot your GomRegistry at specific versions based on the provided metadata. Then it performs a diff between two versions of the registry to determine what migration work must be done.

You can just as easily use a SqlMigration with custom SQL scripts. This stuff is all highly composable now to get exactly what you need.

Live List Models

I’ve written many ways to get live SQLite results into GTK over the past two decades. I think one of the first was a GtkTreeModel implementation for GTK 2 which could do it. With that in mind, it was still rather annoying when making Manuals so I set off to make that convenient.

We have GomRecordListModel, GomEntityListModel, GomRelatedListModel, GomQueryModel all of which have practical uses based on application needs.

But in short, most of those are lazy and support transaction-backed stable identities for entities. Very useful when you have a list of items and an editor loaded in another frame, both of which must reflect the same data.

Expression Trees

This time around I implemented proper expression trees. They model the query, relations, and projections in a manner that allows the driver to lower into a query much more accurately.

You can model things like function-calls cleanly all of which required writing manual SQL before. If you did anything outside of what gom could generate previously, it became madness to maintain.

Vectors

This version of libgom embeds the vec1 extension for SQLite. That means we can store vectors in your records and query them. GomVector makes that easier to manage as a property within your application entity.

I can think of a few things this will be useful for, maybe you can too.

Profiling

This version of libgom has profiling support with another project of mine, Sysprof. The whole library emits profiler marks about what is going on so that it is easy for you to figure out why something might be slow in your application.

Since we’ve already done the integration of Sysprof into GLib/GObject, GTK, Pango, Libdex, and GNOME Shell/Mutter you can very quickly get an idea with details of what is going on in your application. Click record, select the problem area, zoom, and it is often pretty clear. You can have flamegraphs, callgraphs, and timing marks all in one place.

A screenshot of Sysprof in the marks section. A zoomed in timeline across the top, which marks in rows sorted by group/category/name. The timeline boxes show the time region they occupied. In the boxes are the message associated with the mark.

Local First with Sync Coordination

One of my personal motivations for this is around building a native sync protocol for applications I’m building. I wrote numerous SQLite-based sync protocols for the now defunct catch.com before they were acquired by apple. That means I know multiple wrong ways to do it.

This time around, I want to put it right in the data-mapper at the point where you have the most insight. So libgom has the right abstractions in place to build that. The GomSyncCoordinator manages the process and GomSyncTransport is the abstraction-point for service integration.

You work with GomDelta at this layer. The application can provide you with a GomMergePolicy to help make decisions which allow for contextually doing the right thing.

This part is still very new. I’m still building the other side of it but landing the shape early allows me to mock and test things comprehensively before committing to the ABI.

My goal is building a practical, robust, and correct implementation for personal local first features.

A small personal note: as I wrote in my recent update from France, I am no longer employed by Red Hat. Work like this is currently self-funded, out of pocket, while my family and I settle into a new chapter. If you find it useful, a note of encouragement or a contribution means a lot right now. It helps make it possible to keep improving the free software infrastructure many of us rely on.

Stackless Coroutines in Libdex

Fibers are always a nice way to keep your async C code clean while using Libdex. However, occasionally you may want a lighter option which doesn’t require a stack or saving registers for work doing little more than coordinating futures.

I’ve added Stackless Coroutines for this which still allows writing future-coordinating code. Though this will suspend/resume your coroutine by re-entering the function and jumping to the next position. Your threads stack is reused. State is saved in your closure state.

This isn’t a new concept. It is really old just like fibers. What is useful is that this style of continuation passing may still be represented as a DexFuture and therefore composed like the others.

You can place these stackless coroutines in DexTaskGroup alongside fibers, threadpool work, and others. Cancellation will propagate to a clean exit point of the coroutine just like it would with a fiber.

Overhead is a bit lower than fibers in synthetic benchmarks depending on use. I was actually impressed our fiber implementation performed as well as it did head-to-head.

To make building your coroutine continuation easier, libdex provides a handy macro to create your typedef struct, _new(), and _free() helpers in a single macro expansion using DEX_DEFINE_CLOSURE_TYPE().

You use it like this:

DEX_DEFINE_CLOSURE_TYPE (MyTaskState, my_task_state,
  DEX_DEFINE_CLOSURE_VALUE (gsize, bytes),
  DEX_DEFINE_CLOSURE_POINTER (GBytes *, bytes_obj, g_bytes_unref),
  DEX_DEFINE_CLOSURE_OBJECT (GSocketConnection, conn))

Coroutines cannot use the exact syntax that fibers do for awaiting, which is a bummer, but a side-effect of trying to make something that works across Linux, Windows, FreeBSD, Solaris, macOS, etc. Particularly because the implementation must use switch/case to stay portable without address-of-label support on MSVC nor clang-cl.exe.

So awaiting is a bit more clear you’re suspending/resuming the stackless coroutine.

DEX_DEFINE_CLOSURE_TYPE (LoadState, load_state,
  DEX_DEFINE_CLOSURE_OBJECT (GFile, file),
  DEX_DEFINE_CLOSURE_OBJECT (GFileInputStream, input),
  DEX_DEFINE_CLOSURE_OBJECT (GFileInfo, info),
  DEX_DEFINE_CLOSURE_VALUE (int, io_priority))

static DexFuture *
do_something (DexCoroutineContext *context,
              gpointer             user_data)
{
  LoadState *state = user_data;
  g_autoptr(GError) error = NULL;

  DEX_COROUTINE_BEGIN (context);

  DEX_COROUTINE_SUSPEND_OBJECT (
    &state->input, &error,
    dex_file_read (state->file, state->io_priority));

  if (error != NULL)
    return dex_future_new_for_error (g_steal_pointer (&error));

  DEX_COROUTINE_SUSPEND_OBJECT (
    &state->info, &error,
    dex_file_input_stream_query_info (
      state->input,
      G_FILE_ATTRIBUTE_STANDARD_SIZE,
      state->io_priority));

  if (error != NULL)
    return dex_future_new_for_error (g_steal_pointer (&error));

  /* maybe do something useful here */

  return dex_future_new_int64 (g_file_info_get_size (state->info));

  DEX_COROUTINE_END;
}

You do need to be careful about placing things on the stack, because they wont be there on the other side of that DEX_COROUTINE_SUSPEND_* macro expansion. That is because when the scheduler jumps back into your stackless coroutine, it will use a switch/case to jump to the next bit of code. Don’t fear though, just add your state to your continuation which we’ve established is easy to do now.

If you don’t like these macros, you can do things the manual way using dex_coroutine_context_suspend() and dex_coroutine_context_resume() who’s APIs are not terrible either. They do require you make up your own program-counter regime though which for the macro case is basically just __COUNTER__.

You can spawn your coroutine using dex_scheduler_spawn_coroutine() or as part of a work-queue in DexLimiter with dex_limiter_run_coroutine().

dex_scheduler_spawn_coroutine (
  dex_thread_pool_scheduler_get_default (),
  my_coroutine,
  my_coroutine_new (),
  (GDestroyNotify) my_coroutine_free);

I hope you've enjoyed this attempt to make another 1970s technology useful in a modern world.

Libdex Improvements

libdex 1.2 is still in pre-alpha phase but it is also far enough along that it is worth talking about the direction: libdex is growing from a library of future and fiber helpers into a more complete concurrency toolkit.

The most important 1.2 theme is that applications can now describe not just what work should happen concurrently, but how that work should be bounded and owned. DexLimiter lets a workload run with a fixed concurrency budget, with dex_limiter_run() handling the common fiber case by acquiring a permit before work starts and releasing it after the fiber completes. For larger workflows, DexTaskGroup gives related futures a structured scope that can be closed, awaited, or cancelled as one unit.

That combination makes cleanup much easier to reason about when a workflow has many moving pieces. A loader can start many subtasks, keep only a useful number active at once, and return a single future representing the whole operation. If the window closes, the project changes, or the operation times out, the group gives the application one place to cleanly shut the work down.

static DexFuture *
load_many_files (GPtrArray *files)
{
  g_autoptr(DexTaskGroup) group = dex_task_group_new (0);
  g_autoptr(DexLimiter) limiter = dex_limiter_new (8);

  for (guint i = 0; i < files->len; i++)
    {
      GFile *file = g_ptr_array_index (files, i);

      dex_task_group_add (group,
                          dex_limiter_run (limiter,
                                           NULL,
                                           0,
                                           load_one_file,
                                           g_object_ref (file),
                                           g_object_unref));
    }

  return dex_future_with_timeout_seconds (dex_task_group_close (group), 10);
}

There is also a new DexThreadPool for the cases that are not naturally fiber-shaped. Fibers and schedulers are still the right fit for cooperative async work, but many applications need to integrate blocking libraries, database clients, filesystem helpers, or other foreign code. A fixed pool of reusable OS threads, dex_thread_pool_submit(), and asynchronous dex_thread_pool_close() give that integration story a bounded queue and an explicit shutdown path.

Deadlines are another practical piece of the same story. The new timeout wrappers, including dex_future_with_timeout_seconds() and dex_future_with_deadline(), turn time limits into ordinary future composition. Instead of open-coded timeout state spread across an application, a future can resolve normally, reject normally, or reject with DEX_ERROR_TIMED_OUT when the deadline wins.

On the I/O side, 1.2 continues filling in the operations that make responsiveness easier to preserve. dex_aio_open() and dex_aio_close() matter because even operations that look small can stall when they touch the kernel, storage, or network-backed filesystems. Keeping those calls in libdex’s file-descriptor AIO model makes it easier to keep them off the UI thread, using io_uring where it is available and the fallback AIO backend elsewhere.

The broader GIO coverage is intentionally less surprising, but still important. More app launching, GFile, stream, socket, resolver, proxy, TLS, DTLS, permission, subprocess, and Unix-facing APIs now have future-first wrappers. That is the kind of coverage people should expect from libdex over time: not every wrapper needs a release headline, but each one reduces the pressure to leave the future model for common GNOME application work.

((lib)Re)bonjour

I made another weird side project while unemployed. In fact I’ve wanted it for a while but once I learned that “Rebonjour” is the word for “hello again” I just had to finish the library.

librebonjour is an asynchronous DNS-SD and mDNS client library for GLib applications. Or, more practically, it is a small GObject API over the two local service-discovery providers you are likely to find on a Linux system: Avahi and systemd-resolved.

It does not link against either of them. It only talks to them over D-Bus.

The reason for that is mostly boring, which is usually where the useful things are. Applications should not need to care if a machine has Avahi running, or if it is using systemd-resolved for mDNS. They should be able to discover a service, resolve it, maybe advertise something, and get on with whatever they were actually trying to do.

So RebonjourClient selects a backend internally. If org.freedesktop.Avahi is available on the system bus, it uses Avahi. If not, it falls back to systemd-resolved’s org.freedesktop.resolve1 API. If neither is around, availability checks fail like you would expect.

The public API stays the same either way.

What It Does

There are three common things I wanted to make pleasant.

First, one-shot discovery. Ask for the service types in local, ask for instances of something like _ipp._tcp, then resolve one of those instances into addresses and TXT metadata.

Second, browser-style discovery. A RebonjourBrowser owns a stable GListModel of RebonjourService objects. That fits nicely into GTK code because the model object can stay the same while the contents change underneath it.

Third, registration. You can describe a local service with RebonjourServiceDescription, register it, and keep the returned RebonjourRegistration alive for as long as the service should be advertised.

Resolving a service gives you a RebonjourResolvedService. That contains the SRV result, TXT data, priority, weight, and a model of RebonjourEndpoint objects. The endpoints hold the GSocketAddress you would actually use to connect.

Why Two Backends

Avahi is the nicer backend for browsing. Its D-Bus API gives you long-lived browser objects and emits signals when services appear and disappear. That maps very naturally to GListModel changes.

systemd-resolved is different. It has useful DNS-SD and mDNS operations over D-Bus, but the browsing side is lookup-based. That means you can ask what is there, but you do not get the same live add/remove signal stream that Avahi provides.

I did not want applications to have to care about that distinction unless they really want to. So the browser has auto-refresh and refresh-interval properties. With Avahi, auto-refresh is effectively harmless because the model is already live. With systemd-resolved, it starts an internal refresh loop and updates the model for you.

It is not magic. It is just putting the backend-shaped unpleasantness in one place so application code can stay boring.

Asynchronous with libdex

The whole thing is built on libdex. Anything that might touch D-Bus or the network returns a DexFuture.

That means construction, availability checks, service-type lookup, instance lookup, resolving, registration, browser refresh, and unregistering are all future-based. If you are already writing fiber-style code with libdex, the API fits into that directly:

[code language=”c”]
g_autoptr(RebonjourClient) client = NULL;
g_autoptr(GListModel) services = NULL;
g_autoptr(GError) error = NULL;

if (!(client = dex_await_object (rebonjour_client_new (), &error)))
g_error ("%s", error->message);

services = dex_await_object (rebonjour_client_lookup_instances (client,
0,
"_ipp._tcp",
NULL,
REBONJOUR_LOOKUP_FLAGS_NONE),
&error);
[/code]

The 0 there means any interface. Passing NULL for the domain uses local. The common case should not require looking up interface indexes which I’m pretty sure most people reading this have never even done before.

Advertising

Advertising is where things get more system-policy-oriented.

With Avahi, registration goes through Avahi’s D-Bus API. With systemd-resolved, registration uses RegisterService and UnregisterService, which are polkit-protected. Also, resolved needs full mDNS enabled with MulticastDNS=yes; MulticastDNS=resolve is enough to browse and resolve, but not enough to respond as a service.

So librebonjour can expose one API for registration, but it cannot make host policy disappear. Applications still need to handle authorization failure, missing mDNS responder support, sandbox boundaries, or whatever policy the system administrator has decided is appropriate.

That seems like the right way to demarcate things. The library should hide the provider mechanics, not the permissions of the platform.

Why

Mostly because I wanted this to exist.

DNS-SD is handy. Local-network service discovery is still useful. But using it from a GLib application means either caring too much about the provider or writing just enough glue that every application gets to have its own slightly different version of the same code.

And even worse is having to bundle things to build projects like Avahi for Flatpak when you only use the library which calls into D-Bus anyway.

This is not a grand platform initiative. It is not something I am employed to maintain. So you know, use wisely.

Asynchronous Varlink with varlink-glib

I’ve been putting together varlink-glib, which is a library for writing Varlink clients and services in C. The basic idea is to keep the transport policy out of the library. You get a connected GIOStream however you want, whether that is GLib networking, socket activation, or something more specialized, and then wrap it in a VarlinkClientProtocol or VarlinkServerProtocol.

The API is built around DexFuture, which makes the async parts feel a lot nicer in C than the usual callback layering. Client calls return a future, server replies return a future, and internally the protocol can use fibers for the work that wants to look sequential while still integrating with the GLib main context. This is very much the style of API I want for system services: explicit enough that you can debug it, but not so painfully manual that every call site becomes a state machine.

[code lang=”c”]
future = example_calc_add_call_invoke (proxy,
call,
VARLINK_METHOD_CALL_DEFAULT,
add_reply_cb,
NULL,
NULL);
[/code]

There is also a varlink-codegen tool which takes a .varlink interface and generates typed C wrappers for it. That gives you proxy objects, server skeletons, call objects, reply objects, and error constants instead of making every application hand-roll JSON. You can still drop down to VarlinkMessage, VarlinkMessageBuilder, and VarlinkMessageReader for forwarding or weird infrastructure cases, but most code should get to stay typed.

File descriptor passing works when the transport is a Unix socket connection. This follows the same general model as systemd’s Varlink support: the JSON payload contains an integer index, while the actual descriptors are sent out-of-band with SCM_RIGHTS and attached to the containing message as a GUnixFDList. Generated code can continue to treat the field as an integer, while the actual descriptor list stays attached to the underlying VarlinkMessage.

[code lang=”c”]
fd_list = g_unix_fd_list_new ();
fd_index = g_unix_fd_list_append (fd_list, fd, &amp;error);

varlink_message_set_unix_fd_list (parameters, fd_list);
[/code]

Protocol upgrades are supported too. A method call can ask to upgrade the connection, and once the final successful reply is sent, Varlink stops being valid on that connection. The VarlinkProtocol is still a GIOStream, so the next protocol can continue reading and writing through the same object. That keeps the handoff explicit without requiring a separate transport abstraction.

I also wired in optional Sysprof capture support. When enabled, client and server RPCs can show up as Sysprof marks with useful bits like method name, result, reply count, one-way, multi-reply, upgrade, Varlink error, and GError details. That matters because once you have concurrent calls, generated dispatch through VarlinkServerRegistry, and services doing real work, “it got slow somewhere” is not enough information.

A screenshot of Sysprof showing captured varlink calls over time with the RPC and message metadata inline.

There is still more polish to do, but the shape is there: typed generated APIs for normal users, low-level message APIs for infrastructure, DexFuture for async flow, Unix FD passing for the system service cases, protocol upgrades for handoff cases, and profiler hooks so it can be debugged when the happy path stops being happy.

Numbers

So what does that look like performance wise you might ask? For a very simple Echo interface in both D-Bus and Varlink you can get a rough estimate. No daemons, just serialization on a socketpair(). I haven’t started performance tuning yet so there may be ground to make up on both sides. But the answer is that the testcase for varlink-glib is about 5x faster than the testcase for GDBusConnection in either synchronous or asynchronous modes.

This doesn’t apply to all use-cases of D-Bus of course. But for a specific case I use it for (P2P IPC between peer processes), it is pretty big difference.

A graph showing the performance differences between Varlink-GLib and GDBusConnection.

A small personal note: as I wrote in my recent update from France, I am no longer employed by Red Hat. Work like this is currently self-funded, out of pocket, while my family and I settle into a new chapter. If you find it useful, a note of encouragement or a contribution means a lot right now. It helps make it possible to keep improving the free software infrastructure many of us rely on.

Translating French

I have been spending more time learning French lately, and as often happens, that turned into a small side project. liblingua is not intended to be a big mainstream translation platform. It is a fun GLib/GObject library for experimenting with local machine translation from applications.

The library uses Bergamot from Mozilla for the translation backend. Instead of sending text to a web service, liblingua resolves the language pair you ask for, downloads the required language model into the local user cache, and then performs translation locally.

The high-level API is built around a few small objects: LinguaRegistry discovers available translation profiles, LinguaProfile represents a model that can be loaded or downloaded, LinguaProgress reports download progress, and LinguaTranslator performs the translation.

All potentially blocking work is exposed as DexFuture, so it fits naturally into libdex based applications. If you are already using fibers, the code can stay linear and easy to read with dex_await_object().

A Small Example

Here is the basic shape of translating French into English:

[code lang=”c”] #include <liblingua.h> static void translate_example (void) { g_autoptr(LinguaProgress) progress = lingua_progress_new (); g_autoptr(LinguaRegistry) registry = NULL; g_autoptr(LinguaProfile) profile = NULL; g_autoptr(LinguaTranslator) translator = NULL; g_autoptr(LinguaTranslation) result = NULL; g_autoptr(GListModel) profiles = NULL; g_autoptr(GError) error = NULL; if ((registry = dex_await_object (lingua_registry_new (), &error)) && (profiles = lingua_registry_resolve (registry, "fr", "en")) && (profile = g_list_model_get_item (profiles, 0)) && (translator = dex_await_object (lingua_profile_load (profile, progress), &error)) && (result = dex_await_object (lingua_translator_translate (translator, "Bonjour"), &error))) g_print ("%s\n", lingua_translation_get_translation (result)); else g_printerr ("Error: %s\n", error->message); } [/code]

The first time a model is needed, loading the profile may download it. After that, the model is reused from the local cache. That makes liblingua useful for little tools, demos, and desktop experiments where local translation is preferable to wiring everything through a remote service.

In the future this is probably the type of thing we would want as a desktop service to avoid duplicating caches amongst Flatpak applications. It would also be extremely useful to do live translation in Camera and Image Preview apps. I played a bit with that using Tesseract for OCR and it worked better than expected.

Limiters in libdex

Libdex now has DexLimiter, a small utility for bounding how much asynchronous work runs at once.

This is useful when a workload can produce more parallelism than the underlying machine, subsystem, or service should actually handle. Common examples include indexing files, downloading URLs, generating thumbnails, parsing documents, or querying a service with a fixed concurrency budget.

The usual API is dex_limiter_run(). It acquires a permit, starts a fiber, and releases the permit when that fiber finishes.

[code lang=”c”] static DexFuture * load_one_file (gpointer user_data) { GFile *file = user_data; return dex_file_load_contents_bytes (file); } DexLimiter *limiter = dex_limiter_new (8); DexFuture *future = dex_limiter_run (limiter, NULL, 0, load_one_file, g_object_ref (file), g_object_unref); [/code]

In this example, no more than eight file loads will run at the same time, regardless of how many files are queued. The returned DexFuture resolves or rejects with the result of the spawned fiber.

One important detail is that dropping the returned future does not cancel a fiber that has already started. Once work has acquired a permit, it is allowed to complete so that the limiter can release the permit cleanly.

For more specialized cases, DexLimiter also supports manual acquire and release:

[code lang=”c”] g_autoptr(GError) error = NULL; if (dex_await (dex_limiter_acquire (limiter), &amp;error)) { do_limited_work (); dex_limiter_release (limiter); } [/code]

This is useful when the limited section is not naturally represented by a single fiber. However, callers must release exactly once for every successful acquire. In most cases, dex_limiter_run() is preferable because it handles release on both success and failure paths.

The limit should describe the constrained resource, not the number of items being processed. Remote APIs and databases may need a small limit. CPU-heavy work should usually be near the amount of useful worker parallelism. Local I/O can often tolerate a larger value, depending on the storage system. Separate resources should usually have separate limiters, so one workload does not consume another workload’s concurrency budget.

Finally, dex_limiter_close() can be used during shutdown. Once closed, pending and future acquisitions reject with DEX_ERROR_SEMAPHORE_CLOSED. Work that already holds a permit may continue, but releasing after close does not make new permits available.

The goal is to make bounded parallelism simple: queue as much asynchronous work as you need, but only run as much of it as the system should handle.

A Small Update from France

For about the past month, I have no longer been with Red Hat.

That is a strange sentence to write after so many years, but life has a way of changing the scenery whether or not one has finished packing. My family and I have made it safely to France, and we are quite happy here. The light is different, the pace is different, and there is a great deal to learn. For now, that is exactly where our attention needs to be.

I also think there is a broader lesson here for people whose safety, immigration status, or family stability may depend on employer flexibility. Do not assume that long tenure, remote work history, or prior verbal guidance will be enough. My own experience left me with the uncomfortable conclusion that these processes can become very narrow exactly when the human stakes are highest. Get things in writing, understand the policy surface area, and protect your family first.

This also means that some of the things I wrote about in my earlier mid-life update remain unresolved. I am currently in France on a visitor visa, which does not authorize work here. Our focus is on integration, language learning, and getting ourselves properly settled for the long term. That takes time, patience, paperwork, and a certain tolerance for being a beginner again.

As a result, I will not be taking on ongoing software maintenance responsibilities for the foreseeable future. I may still scratch the occasional itch where it directly improves my own computing life, but I am not currently able to provide the kind of broad, sustained stewardship that many projects deserve.

That is not an easy thing to say. Open source is entering an especially difficult period. We are now seeing AI systems used not only to generate code, but also to probe, disrupt, and attack critical software infrastructure. I suspect this will have a negative effect on the maintenance burden for a lot of projects, particularly the foundational pieces that distributions, companies, and users all rely on without always seeing the people behind them.

But there is a limit to what can reasonably be carried as unpaid labor, especially when the primary financial beneficiaries are downstream organizations with considerably more resources than the individual maintainers doing the work. At the moment, I need to prioritize my family, our stability, and the next chapter of our life here.

That said, I am still reachable for appropriate professional inquiries, advisory conversations, or consulting opportunities where the structure and location make sense. The best address for that now is christian at sourceandstack dot com.

For now, we are safe, settling in, and doing our best to build something durable out of a rather odd moment in the world. There are worse places to begin again than France.

A calm Mediterranean shoreline at sunset, with gentle waves rolling onto a pebbled beach beneath a wide blue sky streaked with soft clouds and pastel pink light on the horizon.

Mid-life transitions

The past few months have been heavy for many people in the United States, especially families navigating uncertainty about safety, stability, and belonging. My own mixed family has been working through some of those questions, and it has led us to make a significant change.

Over the course of last year, my request to relocate to France while remaining in my role moved up and down the management chain at Red Hat for months without resolution, ultimately ending in a denial. That process significantly delayed our plans despite providing clear evidence of the risks involved to our family. At the beginning of this year, my wife and I moved forward by applying for long-stay visitor visas for France, a status that does not include work authorization.

During our in-person visa appointment in Seattle, a shooting involving CBP occurred just a few parking spaces from where we normally park for medical outpatient visits back in Portland. It was covered by the news internationally and you may have read about it. Moments like that have a way of clarifying what matters and how urgently change can feel necessary.

Our visas were approved quickly, which we’re grateful for. We’ll be spending the next year in France, where my wife has other Tibetan family. I’m looking forward to immersing myself in the language and culture and to taking that responsibility seriously. Learning French in mid-life will be humbling, but I’m ready to give it my full focus.

This move also means a professional shift. I’ve dedicated a substantial portion of my time to maintaining and developing key components across the GNOME platform and its surrounding ecosystem. These projects are widely used, including in major Linux distributions and enterprise environments, and they depend on steady, ongoing care.

For the better part of two decades I’ve been putting in well beyond forty hours each week maintaining and advancing this stack. That level of unpaid or ad-hoc effort isn’t something I can sustain, and my direct involvement going forward will be very limited. Given how widely this software is used in commercial and enterprise environments, long-term stewardship really needs to be backed by funded, dedicated work rather than spare-time contributions.

If you or your organization depend on this software, now is a good time to get involved. Perhaps by contributing engineering time, supporting other maintainers, or helping fund long-term sustainability.

The folliwing is a short list of important modules where I’m roughly the sole active maintainer:

  • GtkSourceView – foundation for editors across the GTK eco-system
  • Text Editor – GNOME’s core text editor
  • Ptyxis – Default terminal on Fedora, Debian, Ubuntu, RHEL/CentOS/Alma/Rocky and others
  • libspelling – Necessary bridge between GTK and enchant2 for spellcheck
  • Sysprof – Whole-systems profiler integrating Linux perf, Mesa, GTK, Pango, GLib, WebKit, Mutter, and other statistics collectors
  • Builder – GNOME’s flagship IDE
  • template-glib – Templating and small language runtime for a scriptable GObject Introspection syntax
  • jsonrpc-glib – Provides JSONRPC communication with language servers
  • libpeas – Plugin library providing C/C++/Rust, Lua, Python, and JavaScript integration
  • libdex – Futures, Fibers, and io_uring integration
  • GOM – Data object binding between GObject and SQLite
  • Manuals – Documentation reader for our development platform
  • Foundry – Basically Builder as a command-line program and shared library, used by Manuals and a future Builder (hopefully)
  • d-spy – Introspect D-Bus connections
  • libpanel – Provides IDE widgetry for complex GTK/libadwaita applications
  • libmks – Qemu Mouse-Keyboard-Screen implementation with DMA-BUF integration for GTK

There are, of course, many other modules I contribute to, but these are the ones most in need of attention. I’m committed to making the transition as smooth as possible and am happy to help onboard new contributors or teams who want to step up.

My next chapter is about focusing on family and building stability in our lives.