Threaded Spellchecking

Last I mentioned I was doing an ABI cleanup of libspelling as it was really just a couple hour hack that got used by people.

Part of that was to make way for performing spellchecking off the GTK thread. That just landed in today’s libspelling 0.3.0 release. In one contrived benchmark, spellchecking a 10,000 line document was 8x faster. YMMV.

One of the reasons it’s faster is we don’t need to use the GtkTextIter API. Pango provides all the things we need to do that without the PangoLayout overhead. So we just skip right past that and implement iterators off-thread.

I also extracted the GtkTextBuffer adapter so that we may add a GtkEditable adapter in the future. Internally, the adapters provide a series of callbacks to the SpellingEngine.

You might wonder what happens if you make an edit that collides with a region being spellchecked off thread? The answer is quite simple, it checks for collisions and either adapts by adjusting offsets or discards irreconcilable collisions.

Both Text Editor and Builder have been migrated to using libspelling instead of its ancestor code. If you’re running nightly Flatpak of those, take a crack at it and report bugs.

“It testing could use.”

Red Hat Day of Learning

Occasionally at Red Hat we have a “Day of Learning” where we get to spend time learning about technology of our choice.

I spent some time listening to various AI explanations which were suggested readings for the day. Nothing too surprising but also not exactly engaging to me. Maybe that’s because I grew up with a statistics professor for a father.

So while that was playing I spent a little time learning how the GitLab API works. Immediately it stood out that one of the primary challenges in presenting UI for such an API would be in bridging GListModel to their implementation.

So I spent a little time on the architecture for how you might want to do that in the form of a Gitlab-GLib library.

Pagination

There are essentially two modes of pagination supported by GitLab depending on the result set size. Both methods use HTTP headers to denote information about the result set.

Importantly, any design would want to have an indirection object (and in this case GitlabListItem) which can have it’s resource dynamically loaded.

Resources (such as a GitlabProject, or GitlabIssue) are “views” into the JSON result set. They are only used for reading, not for mutating or creating resources on the API server.

Offset-based Pagination

This form is somewhat handy because you can know the entire result-set size up front. Then you can back-fill entries as accessed by the user using bucketed lazy-loading.

When the bucketed page loads, the indirection objects are supplied with their resource object which provides a typed API over the JsonNode backing it.

This is the “ideal” form from the consuming standpoint but can put a great deal of load on the GitLab server instance.

Progressive Pagination

The other form of pagination lets you fetch the next page after each subsequent request. The header provides the next page number.

One might expect that you could use this to still jump around to the appropriate page. However if you are not provided the “number of pages” header from the server then there is not much you can do to clamp your page range.

Conclusion

This was a fun little side-project to get to know some of the inner workings of the API backing what those of us in GNOME use every day. I have no idea if anything will come of it, but it certainly could be useful from Builder if anyone has time to run with it.

For example, imagine having access to common GitLab operations from the header bar.

A screenshot of GNOME Builder with a new GtkMenuButton in the headerbar containing a GitLab icon and menu.

Refreshed Search

Builder got a refreshed search popover. It’s not even a GtkPopover anymore and instead uses AdwDialog.

You can use some of the typical “prefixes” to filter search results or do nothing and get everything mixed together.

A screenshot of the search popover displaying a list of filters such as @ to display symbols or ~ to filter only filenames.

For example, prefix the search with @ to limit the results to indexed symbol names. Quick preview is still presented side-by-side.

A popover is displayed filtering results to symbols using the @ prefix.

You can also search for documentation now if jumping to the search panel is too much work. Just prefix with ? and you’re ready to go.

The search popover displaying a list of documentation options from DexFuture.

Sometimes it can be handy to run various build actions using the search popover as well. Many of the menu items are searchable. Just prefix the search query with >.

The search popover showing a number of menu items and their action description along with associated keyboard shortcuts.

Enjoy!

Custom Artifacts Directories

Since the inception of Builder, it has used $XDG_CACHE_DIR for many things like custom installation paths, flatpak-builder state directories, out-of-tree build directories, code indexes, and more.

I finally got around to allowing for custom cache roots and now Builder will default to ~/Projects/.gnome-builder/. If you’ve set a custom projects directory then .gnome-builder within that will be used.

You may also change this value globally or per-project in Preferences and/or Project Settings.

A screenshot of the application preferences in the General panel of Build Tooling section. The first option is the build artifacts directory which may be set by the user.

Sidebars in Libpanel

One of the more recent design trends in GNOME has been the use of sidebars. It looks great, it’s functional, and it gives separation of content from hierarchy.

A screenshot showing a number of GNOME applications which contain sidebars including Nautilus, D-Spy, Control Center, and Calendar. The image contains both light and dark variants split by a line from lower left to upper right of the image.

Builder, on the other hand, has been stuck a bit closer to the old-hat design of IDEs where the hierarchy falls strictly from the headerbar. This is simply because libpanel was designed before that design trend. Some attempt was made in Builder to make it look somewhat sidebar’ish, but that was the extent of it given available time.

A screenshot of the GNOME 45 release of Builder where the headerbar is across the top and panels, documents, and project panel below.

Last week I had a moment of inspiration on a novel way we could solve it without uprooting the applications which use libpanel. You can now insert edge widgets in PanelDockChild which are always visible even when the child is not. Combining that with being able to place a headerbar inside a PanelDockChild along with your PanelFrames means you can get something that looks more familiar in modern GNOME.

A screenshot of what will become Builder for GNOME 46 which includes the common sidebar styling.

If you’d like to improve things further, you know where to find the code.

GJS plugins in GNOME Builder

As I mentioned in my last post, Builder has switched to GJS as it’s dynamic language for plugins. We already support a number of compiled languages including C, C++, Rust, and Vala.

Previously we had used PyGObject. Do to the lack of GTypeInstance support in PyGObject, that isn’t an option currently. I already ported all of Builder’s plugins written in Python to C over the course of a week last summer. That ended up making things both more stable and allow us to ship the GTK 4 port on time.

This past year I wrote a new async/futures framework for GLib called libdex which provides Fibers, Futures, Channels, await, threadpools, io_uring support, and more. That tool heavily uses the same GTypeInstance features that GTK 4 uses.

GJS has improved a lot over the years due to how it is being maintained and it’s importance in the GNOME Shell stack. I’d like to double down on that so Builder can benefit from their hard work. Therefore, if you want to write plugins in JavaScript and maintain them upstream, that’s something I’m happy to see happen.

You can see some examples for how to write a JavaScript plugin for Builder in the examples directory.

GJS plugins for libpeas-2.0

One of the main features I want to land for the libpeas-2.0 ABI break is support for plugins in JavaScript.

With the right set of patches, you can get that. Thanks to Philip Chimento, GJS will hopefully soon land support for running code in a SpiderMonkey realm. Philip also did us a solid and wrote the code to exfiltrate enough GType information from an imported JavaScript module. That allows libpeas to correlate which GTypes are provided by a plugin.

With the GJS realm support in place, we can land the new GJS loader for libpeas-2.0.

My personal goal for this is to enable JavaScript-based plugins in GNOME Builder. With how much GJS has improved over the years to support GNOME Shell, it is probably our most-maintained language binding for a dynamic language with modern JIT features.

For example, if you wanted to make an addin in Builder which responded to changes of a file within the editor, you might write something like this as your plugin. Keep in mind I’m not a JavaScript developer and GJS developers may tell you there are fancy new language features you can use to simplify this code further.

import GObject from 'gi://GObject';
import Ide from 'gi://Ide';

export var TestBufferAddin = GObject.registerClass({
    Implements: [Ide.BufferAddin],
}, class TestBufferAddin extends GObject.Object {

    vfunc_language_set(buffer, language_id) {
        print('language set to', language_id);
    }

    vfunc_file_loaded(buffer, file) {
        print(file.get_uri(), 'loaded');
    }

    vfunc_save_file(buffer, file) {
        print('before saving buffer to', file.get_uri());
    }

    vfunc_file_saved(buffer, file) {
        print('after buffer saved to', file.get_uri());
    }

    vfunc_change_settled(buffer) {
        print('spurious changes have settled');
    }

    vfunc_load(buffer) {
        print('load buffer addin');
    }

    vfunc_unload(buffer) {
        print('unload buffer addin');
    }

    vfunc_style_scheme_changed(buffer) {
        let scheme = buffer.get_style_scheme();
        print('style scheme changed to', scheme ? scheme.get_id() : scheme);
    }
});

You can easily correlate that to the IdeBufferAddin interface definition.

Concurrency, Parallelism, I/O Scheduling, Thread Pooling, and Work-Stealing

Around 15 years ago I worked on some interesting pieces of software which are unfortunately still not part of my daily toolbox in GNOME and GTK programming. At the time, the world was going through changes to how we would write thread pools, particularly with regards to wait-free programming and thread-pooling.

New trends like work-stealing were becoming a big thing, multiple-CPUs with multiple NUMA nodes were emerging on easy to acquire computers. We all were learning that CPU frequency was going to stall and that non-heterogeneous CPUs were going to be the “Next Big Thing”.

To handle those changes gracefully, we were told that we need to write software differently. Intel pushed that forward with Threading Building Blocks (TBB). Python had been doing things with Twisted which had an ergonomic API and of course “Stackless Python” and similar was a thing. Apple eventually came out with Grand Central Dispatch. Microsoft Research had the Concurrency and Coordination Runtime (CCR) which I think came out of robotics work.

Meanwhile, we had GThreadPool which honestly hasn’t changed that much since. Eventually the _async/_finish paring we’re familiar with today emerged followed by GTask to provide a more ergonomic API on top of it.

A bit before the GTask we all know today, I had written libgtask which was more of a Python Twisted-style API which provided “deferreds” and nice ways to combine them together. That didn’t come across into GLib unfortunately. To further the pain there, what we got in the form of GTask has some serious side-effects which make it unsuitable as a general construct in my humble opinion.

After realizing libgtask was eclipsed by GLib itself, I set off on another attempt in the form of libiris. That took a different approach that tried to merge the mechanics of CCR (message passing, message ports, coordination arbiters, actors), the API ergonomics of Python Twisted’s Deferred, and Apple’s NSOperation. It provided a wait-free work-stealing scheduler to boot. But it shared a major drawback of GLib’s GTask (beyond correctness bugs that plague it today). Primarily that thread pools can only process the work queue and therefore if you need to combine poll() or GSource that attach to a GMainContext you’re going to require code-flow to repeatedly bounce between threads.

This is because you can simplify a thread pool worker to while (has_work()) do_work ();. Any GSource or I/O either needs to bounce to the main thread where the applications GMainContext exists or to another I/O worker thread if doing synchronous I/O. On Linux, for a very long time, synchronous I/O was the “best” option if you wanted to actually use the page cache provided by the kernel, so that’s largely what GLib and GIO does.

The reason we couldn’t do something else is that to remove an item from the global work queue required acquiring a GMutex and blocking until an item is available. On Linux at least, we didn’t have APIs to be able to wait on a Futex while also poll()ing a set of file-descriptors. (As a note I should mention for posterity that FD_FUTEX was a thing for a short while, but never really usable).

In the coming years, we got a lot of new features in Linux that allowed improvements to the stack. We got signalfd to be able to poll() on incoming Unix signals. We got eventfd() which allowed a rather low-overhead way to notify coordinating code with a poll()able file-descriptor. Then EFD_SEMAPHORE was added so that we can implement sem_t behavior with a file-descriptor. It even supports O_NONBLOCK.

The EFD_SEMAPHORE case is so interesting to me because it is provides the ability to do something similar to what IRIX did 20+ years ago, which is a pollable semaphore! Look for usnewpollsema() if you’re interested.

There was even some support in GLib to support epoll(), but that seems to have stalled out. And honestly, making it use io_uring might be smarter option now.

After finishing the GTK 4 port of GNOME Builder I realized how much code I’ve been writing in the GAsyncReadyCallback style. I don’t particularly love it and I can’t muster the energy to write more code in that style. I feel like I’m writing top-half/bottom-half interrupt handlers yet I lack the precision to pin things to a thread as well as having to be very delicate with ownership to tightly control object finalization. That last part is so bad we basically don’t use GLib’s GTask in Builder in favor of IdeTask which is smart about when and where to release object references to guarantee finalization on a particular thread.

One thing that all these previous projects and many hundreds of thousands of async-oriented C code written has taught me is that all these components are interlinked. Trying to solve one of them without the others is restrictive.

That brings me to 2022, where I’m foolishly writing another C library that solves this for the ecosystem I care most about, GTK. It’s goal is to provide the pieces I need in both applications as well as toolkit authoring. For example, if I were writing another renderer for GTK, this time I’d probably built it on something like this. Given the requirements, that means that some restrictions exist.

  • I know that I need GMainContext to work on thread pool threads if I have any hope of intermixing workloads I care about on worker threads.
  • I 100% don’t care about solving distributed computing workloads or HTTP socket servers. I refuse to race for requests-per-second at the cost of API ergonomics or power usage.
  • I know that I want work stealing between worker threads and that it should be wait-free to avoid lock contention.
  • Worker threads should try to pin similar work to their own thread to avoid bouncing between NUMA nodes. This increases data cacheline hits as well as reduces chances of allocations and frees moving between threads (something you want minimized).
  • I know that if I have 32 thread pool threads and 4 jobs are added to the global queue, I don’t want 32 threads waking up from poll() to try to take that those work items.
  • The API needs to be simple, composable, and obvious. There is certainly a lot of inspiration to be taken from things like std::future, JavaScript’s Promise, Python’s work on deferred execution.
  • GObject has a lot of features and because of that it goes through great lengths to provide correctness. That comes at great costs for things you want to feel like a new “primative”, so avoiding it makes sense. We can still use GTypeInstance though, following in the footsteps of GStreamer and GTK’s Scene Kit.
  • Cancellation is critical and not only should it cause the work you created to cancel, that cancellation should propagate to the things your work depended on unless non-cancelled work also depends on it.

I’ve built most of the base of this already. The primary API you interact with is DexFuture and there are many types of futures. You have futures for IO (using io_uring). Futures for unix signals. A polled semaphore where dex_semaphore_wait() is a future. It can wrap _async/_finish pairs and provide the result as a Future. Thread pools are efficient in waiting on work (so staying dormant until necessary and minimal thread wake-ups) while also coordinating to complete work items.

There is still a lot of work to go, and so far I’ve only focused on the abstractions and Linux implementations. But I feel like there is promise (no pun intended) and I’m hoping to port swaths of code in Builder to this in the coming months. If you’d like to help, I’d be happy to have you, especially if you’d like to focus on alternate DexAioBackends, DexSemaphore using something other than eventfd() on BSD/Solaris/macOS/Windows, and additional future types. Additionally, working to GLib to support GMainContext directly using io_uring would be appreciated.

You can find the code here, but it will likely change in the near future.

Builder 43.alpha0

It’s been an absolute mad dash this cycle porting Builder to GTK 4, but 43.alpha0 is out and available on GNOME Nightly.

Builder is one of the larger applications within GNOME, especially if you include the libraries I had to write and maintain to make that possible. Porting an application to a new toolkit is always a big undertaking. However, it also provides an opportunity to rethink how major components work and simplify them while you’re there.

So that is what has happened this cycle. It’s going to end up being a much more polished product due to the enormous amount of simplification going on.

GTK 4 has simplified how a lot of things work and provided APIs that feel so obvious when you use them. Of course, that also means lots of code needs to be changed (well deleted, mostly). Having focused heavily on using GListModel in previous releases also paid off massively this cycle.

Anyway, here it is. It’s still missing plenty of features as I dash towards the finish line implementing them as quick as I can.

For those that want to test it out, note that our application-id has changed so that you can install Builder’s stable branch and nightly branch side-by-side.

flatpak --user remote-add --if-not-exists gnome-nightly \
  https://nightly.gnome.org/gnome-nightly.flatpakrepo
flatpak --user install gnome-nightly org.gnome.Builder.Devel

A screenshot of Builder's new about dialog

Builder GTK 4 Porting, Part VII

It’s been another couple weeks of porting, along with various distractions.

The big work this time around has been deep surgery to Builder’s “Foundry”. This is the sub-system that is responsible for build-systems, pipelines, external-devices, SDKs, toolchains, deployments-strategies and more. The sub-system was starting to show it’s age as it was one of the first bits of Builder to organically emerge.

One of the things that become so difficult over the years is dealing with all the container layers we have to poke holes through. Running a command is never just running a command. We have to setup PTYs (and make sure the TTY setup ioctl()s happen in the right place), pass environment variables (but to only the right descendant process), and generally a lot more headaches.

What kicked off this work was my desire to remove a bunch of poorly abstracted bits and we’re almost there. What has helped considerably is creating a couple new objects to help manage the process.

The first is an IdeRunContext. It is sort of like a GSubprocessLauncher but allows you to create layers. At the end you can convert those layers into a subprocess launcher but only after each layer is allowed to rewrite the state as you pop back to the root. In practice this has been working quite well. I finally have control without crazy amounts of argument rewriting and guesswork.

To make that possible, I’ve introduced an IdeUnixFDMap which allows to manage source↔dest FD translations for FDs that will end up in the subprocess. It has a lot of helpers around it to make it fit well into the IdeRunContext world.

All of this has allowed the new IdeRunCommand to really shine. We have various run command providers (e.g. plugins) all of which can seamlessly be used across the sub-systems supporting IdeRunContext. Plugins such as meson can even export unit tests as run commands.

The shellcmd plugin has also been rewritten upon these foundations. You can create custom commands and map them to keyboard shortcuts. The commands, like previous version of Builder, can run in various localities. A subprocess, from the build pipeline, as an app runner, or on the host. What has improved, however, is that they can also be used in surrogate of your projects run command. These two features combined means you can make Builder work for a lot of scenarios it never did before by configuring a few commands.

There aren’t a lot of screenshots for things like this, because ideally it doesn’t look too different. But under the hood it’s faster, more reliable, and far more extensible than it was previously. Hopefully that helps us cover a number of highly requested use-cases.

a screenshot of the debugger

a screenshot of the build menu with debug selected

a screenshot of the run command selection selection dialog

a screenshot showing the location of the select run command menu item

a screenshot editing a command