Samet Demir

Kria KV260 Notes, Part 2: Cutting Ubuntu’s Background Weight

In Part 1 I found that my KV260 was running its microSD card in a slow legacy mode, and fixed it with a one-line device-tree change. That was about storage. This note is about the other half of the problem: memory and background work.

The KV260 has 4 GB of RAM. The official Ubuntu image is a full desktop install, so a lot of that RAM goes to things I will never use on this board: firmware updates, printing, a cellular modem, calendar sync. I wanted those gone. But I also want to test graphical applications later, so GNOME had to stay.

So the goal was not "go headless". The goal was: keep the desktop, remove everything around it that is not doing useful work.

Ground rules

Before touching anything, I decided what must stay:

  • GNOME Shell, GDM, Xorg
  • xdg-desktop-portal-gnome and the portal infrastructure
  • Snap and snapd-desktop-integration (Firefox is a Snap on 22.04)
  • NetworkManager, Wi-Fi, SSH
  • Audio
  • Kria-specific services like dfx-mgr

Everything else was a candidate, but only if I could name what it does and say I do not need it.

Same setup as Part 1: KV260 Rev B, Ubuntu 22.04.5, kernel 5.15.0-1078, logged in to a GNOME session over DisplayPort.

Measure first

Three commands gave me everything I needed:

free -h
systemd-cgtop -b -n 1 -m | head -25
ps -eo pid,user,comm,rss,%mem --sort=-rss | head -25

free gives the system view. systemd-cgtop shows memory per systemd unit. ps shows individual processes.

This was the starting point, with the desktop logged in and idle:

               total        used        free      shared  buff/cache   available
Mem:           3.8Gi       871Mi       1.3Gi       4.0Mi       1.7Gi       2.8Gi
user.slice/user-1000.slice/user@1000.service   1.3G
system.slice                                   526.5M
system.slice/fwupd.service                     154.9M
system.slice/snapd.service                      53.3M

You may notice these two views do not add up: the cgroups alone are about 1.8 GiB, but free says only 871 MiB is used. That is normal. free moves file cache into its own column, while cgroups count cache against the unit that touched it. I will come back to this at the end. For deciding what to disable, the cgroup view is the useful one.

And the biggest processes:

gnome-shell                  219 MiB
fwupd                        170 MiB
snapd-desktop-integration    105 MiB
xdg-desktop-portal-gnome      86 MiB
Xorg                          76 MiB
evolution-alarm-notify        58 MiB
goa-daemon                    38 MiB
tracker-miner-fs-3            31 MiB
evolution-calendar-factory    28 MiB
evolution-addressbook-factory 27 MiB
multipathd                    26 MiB
evolution-source-registry     23 MiB

Two things stood out. First, fwupd — the firmware update daemon — was the second biggest process on the board, bigger than Xorg. Second, the user session was 1.3 GiB, and a good part of that was Evolution, GNOME Online Accounts, and Tracker: calendar, contacts, account sync, and file indexing. None of that is graphics.

First pass: system services

These are normal system units, so this part is easy. I disabled the ones I could confidently say I do not use:

sudo systemctl disable --now \
  fwupd.service fwupd-refresh.timer \
  ModemManager.service \
  cups.service cups-browsed.service cups.path cups.socket \
  avahi-daemon.service avahi-daemon.socket \
  packagekit.service

What each one is, so you can decide for yourself:

Service What it does Keep it if…
fwupd Firmware updates via LVFS you use fwupdmgr on the board
ModemManager Cellular modem support you attach a 4G/5G modem
cups, cups-browsed Printing and printer discovery you print from the KV260
avahi-daemon mDNS / .local name discovery you rely on hostname.local
packagekit Background package management for GUI tools you want GNOME Software update notifications

I left Bluetooth alone: I may test Bluetooth peripherals later, and it uses almost nothing while idle. Undoing any of these is sudo systemctl enable --now <unit>.

One detail: packagekit and colord are static units, started on demand over D-Bus. disable does not really apply to them, and both were still running afterwards. They are small (7 MiB and 21 MiB), so I left them.

Result of this pass, measured right after:

system.slice   526.5M  →  377.4M

About 150 MiB less in system services, almost all of it fwupd.

Second pass: the user session

This is where it got more interesting, because these services run inside the GNOME user session, not as system units. They need systemctl --user, and that has a trap.

The trap

My first attempt over SSH was:

sudo -u ubuntu systemctl --user mask tracker-miner-fs-3.service
Failed to connect to bus: $DBUS_SESSION_BUS_ADDRESS and $XDG_RUNTIME_DIR not defined
(consider using --machine=<user>@.host --user to connect to bus of other user)

sudo -u changes the user, but it does not connect you to that user’s running systemd session. The error message actually gives the answer. The correct form is:

sudo systemctl --machine=ubuntu@.host --user mask tracker-miner-fs-3.service

This talks to the systemd user manager of the ubuntu account that is currently logged in to GNOME. I used this form for everything below.

Tracker

Tracker indexes file contents for GNOME search. On a development board with source trees and build directories, it is mostly background I/O for a feature I do not use.

sudo systemctl --machine=ubuntu@.host --user mask \
  tracker-miner-fs-3.service tracker-extract-3.service tracker-miner-apps-3.service
sudo systemctl --machine=ubuntu@.host --user stop \
  tracker-miner-fs-3.service tracker-extract-3.service tracker-miner-apps-3.service

After this, GNOME file search no longer indexes file contents. Normal file browsing works as before.

Evolution and GNOME Online Accounts

I do not want to remove evolution-data-server or gnome-online-accounts with apt. On Ubuntu they are dependencies of the desktop, and removing them can pull out gnome-shell and gdm. Masking the user services keeps the packages and stops the processes:

sudo systemctl --machine=ubuntu@.host --user mask \
  evolution-alarm-notify.service \
  evolution-addressbook-factory.service \
  evolution-calendar-factory.service \
  evolution-source-registry.service \
  evolution-user-prompter.service \
  goa-daemon.service

Then stop the same list, then reboot. After reboot, all the Evolution processes were gone. But this was still there:

$ pgrep -a -u ubuntu goa-daemon
2283 /usr/libexec/goa-daemon

And the unit said it was masked and inactive:

○ goa-daemon.service
     Loaded: masked (Reason: Unit goa-daemon.service is masked.)
     Active: inactive (dead)

Why masking was not enough for GOA

goa-daemon is not started by its systemd unit. It is started by D-Bus activation: when any GNOME component asks for the bus name org.gnome.OnlineAccounts, D-Bus launches the daemon itself, using the file /usr/share/dbus-1/services/org.gnome.OnlineAccounts.service. The systemd mask never gets a chance.

The clean fix is a per-user override of that D-Bus service file. It takes priority over the packaged one, does not touch anything under /usr, and survives package upgrades:

mkdir -p ~/.local/share/dbus-1/services
cat > ~/.local/share/dbus-1/services/org.gnome.OnlineAccounts.service <<'EOF'
[D-BUS Service]
Name=org.gnome.OnlineAccounts
Exec=/usr/bin/false
EOF

Log out and in again, or reboot. After that, pgrep goa-daemon returns nothing, and it has stayed that way.

What you lose: GNOME Online Accounts integration (Google, Microsoft, Nextcloud and so on), GNOME Calendar and Contacts sync, Evolution. What you keep: everything graphical. Browser sign-in to web services is not affected, because that does not go through GOA.

To undo: rm ~/.local/share/dbus-1/services/org.gnome.OnlineAccounts.service, unmask the units, and reboot.

Result

Same three commands, same GNOME session logged in and idle, after the service and masking changes and a reboot. One honest detail: I took this snapshot just before I noticed the surviving goa-daemon, so these numbers still include its ~38 MiB. The D-Bus fix above came after this measurement.

               total        used        free      shared  buff/cache   available
Mem:           3.8Gi       749Mi       2.3Gi        12Mi       796Mi       2.9Gi
user.slice/user-1000.slice/user@1000.service   597.3M
system.slice                                   323.3M
Before After
user@1000.service (GNOME session cgroup) 1.3 GiB 597 MiB
system.slice 526 MiB 323 MiB
free "used" 871 MiB 749 MiB
free "available" 2.8 GiB 2.9 GiB

The session cgroup went from about 1.3 GiB to 597 MiB — roughly 700 MiB less memory attributed to the GNOME session, while the desktop looks and works exactly the same.

One extra data point I did not plan: I ran the same commands once at the GDM login screen, before logging in. System plus greeter was 413 MiB used, 3.2 GiB available. So a fully idle board with a login screen sits at about 400 MiB, and a logged-in trimmed GNOME session adds about 340 MiB on top.

A note on what these numbers mean

This is the part I promised to come back to. I am careful to say "attributed to the cgroup" and not "saved 700 MB of RAM". free, systemd-cgtop, and ps RSS are three different ways of counting. Cgroup memory includes page cache charged to that unit; RSS double-counts shared libraries; free shows the whole system. They do not add up to one ledger.

A concrete example: a few hours later, snapd.service showed 313 MiB in systemd-cgtop, up from 48 MiB. Nothing was wrong. Snap had refreshed Firefox, and the file cache from the download was charged to snapd‘s cgroup. It goes away under memory pressure. If you only looked at that one number you would think snapd had a leak.

The number I trust most for "is the board healthy" is available in free. It went from 2.8 to 2.9 GiB. The board was never short of memory; the point was to stop it doing pointless work.

Small extras

Three more changes, mostly about reducing writes to the microSD card rather than RAM:

  • Volatile journal. Logs go to RAM and disappear on reboot. /etc/systemd/journald.conf.d/10-kv260-storage.conf with Storage=volatile and RuntimeMaxUse=64M. If you are still debugging something across reboots, use SystemMaxUse=64M instead and keep them persistent.
  • vm.swappiness=10 in /etc/sysctl.d/99-kv260-memory.conf. There is no swap on this image, so this is only future-proofing.
  • Snap revisions. snap set system refresh.retain=2 and remove old disabled revisions with snap list --all. This saves card space, not RAM.

$ ls comments/ | wc -l

Zero comments, which makes the first one the best one by default.

$ make comment

Compile your thoughts

first comment is held until I read it. after that, you're trusted. no cookies, no third parties.