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-gnomeand 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.confwithStorage=volatileandRuntimeMaxUse=64M. If you are still debugging something across reboots, useSystemMaxUse=64Minstead and keep them persistent. vm.swappiness=10in/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=2and remove old disabled revisions withsnap list --all. This saves card space, not RAM.
$ ls comments/ | wc -l
Zero comments, which makes the first one the best one by default.