A developer toolkit for people who write GNOME Shell extensions. It's a native GTK4 / libadwaita app, so it looks and behaves like the rest of your desktop instead of some dev tool bolted on the side.
The way it works: it drops a small bridge extension into the running shell and instruments your extension at runtime, without you touching a single line of your own code. From there you can profile function timing and look at it as a flamegraph (the call tree and timing), a swimlane (when and how often each function runs), or a histogram (where the time actually goes). Logs get filtered down to one extension UUID so you're not digging through the whole journal, and the inspector lets you open up any running extension object and walk its properties, values and nested objects while it runs.
| Feature | Description |
|---|---|
| Extension Manager | Browse all installed extensions with status, enable/disable with one click, open the source folder directly |
| Log Viewer | Live systemd journal stream. Captures the gnome-shell process by default (that's where all extensions actually run), with structured capture controls (scope, boot, source preset, priority) and a raw journalctl override when you want full control. Filter by log level, extension tag, and full-text search in real time |
| Profiler | Monkey-patch any extension at runtime. No code changes needed. Visualise timing as a flamegraph, swimlane, or histogram; export and reload sessions as JSON |
| Inspector | Inspect a live extension object: browse its properties and methods, see current values, and call methods interactively |
The two global shortcuts are handled inside gnome-shell by the bridge, so
they work even when the GSE Profiler window is not focused β handy while you
click around in the extension you're profiling. All other shortcuts are
scoped to the currently selected tab. The full list is available in-app from
the menu β Keyboard Shortcuts (or Ctrl+?).
| Shortcut | Action | Scope |
|---|---|---|
Super+F5 |
Toggle profiling (start / stop) | global |
Super+Shift+F5 |
Restart profiling (stop, clear, start) | global |
Ctrl+1β¦4 |
Switch tab (Details / Profiler / Inspector / Logs) | window |
F9 |
Toggle the extension sidebar | window |
Ctrl+? / F1 |
Show the keyboard-shortcuts dialog | window |
Ctrl+R |
Primary action of the tab (start-stop / refresh) | tab |
Ctrl+F |
Focus the filter / search field | tab |
Ctrl+S |
Save profile / export log | tab |
Ctrl+O |
Load a saved profile (Profiler) | tab |
Ctrl+L |
Clear profiling data / log | tab |
The global shortcuts register only after the bridge extension is installed and GNOME Shell has reloaded (the usual log out / back in).
There are two parts: a GTK4 app (Python) and a bridge GJS extension
(gse-profiler-bridge@todevelopers) that the app installs for you on first launch. The
bridge runs inside the gnome-shell process itself, which is what gives it direct,
in-process access to each loaded extension's live state and the objects reachable from it.
You have to restart GNOME Shell once after the bridge lands. The app just asks you to log out and back in (Wayland only; X11 sessions aren't supported).
The main window has a live connection indicator, so you always know whether the bridge is actually talking to the app.
The app and bridge talk over a Unix domain socket
($XDG_RUNTIME_DIR/gse-profiler/gse-profiler.sock) using newline-delimited JSON. The bridge
initiates the connection and reconnects automatically after a failure (3 s delay). On connect
it sends a hello handshake; from that point the app can start a profiling or inspection session.
Standard extension management (listing, enabling, disabling) goes through the regular
org.gnome.Shell.Extensions D-Bus interface. The socket is only there for the stuff
D-Bus is bad at: high-frequency profiling events and live inspection results. Neither path
needs elevated permissions.
When you start profiling, the bridge takes a snapshot of the live object graph rooted at the
extension's stateObj. It walks methods on each object's prototype chain and follows
object-valued data properties breadth-first. Arrays, Map values, and Set values are
traversed as collections, so module-manager patterns such as Map<string, Module> are covered
too. Breadth-first order matters when a limit is hit: methods sit near the root while bulk data
(cached feed items, parsed entries) sits deeper, so the scan instruments every shallow object
before it starts spending its budget on deep data. A store holding thousands of cached records
therefore truncates its own contents instead of starving the sibling objects that hold the
extension's actual behaviour. Cycle
detection and traversal safety limits keep large or self-referential graphs bounded. Getters
and arbitrary iterators are not invoked because running extension code during discovery could
have side effects.
Every method the bridge successfully wraps is instrumented. A method becomes observed only after it is called while recording; only observed methods have timing rows in the table. The Profiler UI shows both counts so a method that was instrumented but never exercised is not mistaken for a discovery failure. Stop/Start keeps the observed recording until you clear it, while the instrumented count always describes the latest discovery scan.
Discovery is a start-time snapshot. Objects, collection entries, or functions added afterwards are not picked up automatically; restart profiling to clear the recording and scan the current graph again. You don't change anything in your extension's source, and installed wrappers are fully reversed when profiling stops.
Each wrapped call adds two GLib.get_monotonic_time() reads (microsecond precision) and
queues a timing event. The bridge sends a batch every 50 ms or when 256 events are queued,
whichever comes first, to reduce socket overhead. Batches carry a recording generation so a
late batch from before Restart cannot contaminate the fresh recording. For a typical GNOME
Shell extension the overhead is negligible, but extremely tight animation loops or extensions
that invoke hundreds of functions per frame may see a measurable slowdown during recording.
What runtime monkey-patching cannot capture automatically:
- GObject virtual functions (vfuncs)
- Module-level helpers, closures, and other functions held only in lexical variables
- ECMAScript
#privatemethods - Symbol-keyed methods or object-graph links
- Values hidden inside
WeakMaporWeakSet, which JavaScript cannot enumerate - Objects exposed only through getters, because discovery deliberately does not execute getters
- Constructors,
enable(), and other calls that finished before profiling started - Callback references that were bound or copied before their method property was wrapped
- Functions or collection entries added after the start-time snapshot (restart to rescan)
- Async timing: an
asyncfunction is only recorded until it returns itsPromise, usually at the firstawait. The time spent waiting, and the continuations that run after eachawait, don't count toward the event's duration. So async methods show their synchronous setup cost, not their real end-to-end latency.
Profiling only replaces functions on live JavaScript objects inside the running gnome-shell
process; it never modifies the target extension's files. Normally, Stop profiling restores
all wrappers still installed by that recording. If the app is unavailable, disabling the
GSE Profiler Bridge also runs its cleanup.
If the target extension still behaves unexpectedly, disable and enable it to rebuild the
runtime objects managed by its lifecycle. This is a useful first recovery step, but GNOME Shell
may reuse the extension's root instance. Logging out and back in starts a clean gnome-shell
process and is therefore the definitive reset for any remaining in-memory patch. Reinstalling
the target extension alone is unnecessary and does not clear JavaScript code already loaded
into the current Shell process.
| Limit | Value |
|---|---|
| Profiler: max traversal depth | 6 (root is depth 0) |
| Profiler: max visited objects | 2,000 |
| Profiler: max entries per Array/Map/Set | 512 |
| Profiler: max own properties per object | 2,048 |
| Profiler: max instrumented functions | 5,000 |
| Profiler: bridge event batch | 50 ms or 256 events |
| Max recorded events | 50,000 (oldest dropped first) |
| Inspector: max properties per object | 50 |
| Inspector: max string value length | 200 characters |
| Inspector: max array elements shown | 50 |
| Inspector: max Map entries shown | 50 |
| UI refresh batch window | 80 ms |
If any profiler traversal cap is reached, the bridge marks the scan as truncated and the UI labels the instrumentation result as limited.
All three views run on the same event data, they just answer different questions:
Flamegraph. The call tree laid out on a real-time axis. Each bar is one call, width is duration, nesting shows caller/callee depth. Best when you want to see what called what and catch call stacks that are unexpectedly deep or wide.
Swimlane. One horizontal lane per function, with idle gaps squeezed out. Every invocation is its own segment on that function's row, so you can see when and how often a function runs without the call-depth nesting getting in the way.
Histogram. Functions ranked by self-time, the wall-clock time spent in a function's own code, not counting its callees. This is the fastest way to answer where is the time actually going?, because it ignores time that really belongs to the callee.
Requires GNOME Shell 46+ in an active Wayland GNOME session.
Add the self-hosted stable remote once, then install. You get
automatic updates with flatpak update from then on:
flatpak remote-add --user --if-not-exists todevelopers \
https://todevelopers.github.io/flatpaks/todevelopers.flatpakrepo
flatpak install --user todevelopers io.github.todevelopers.GseProfiler
flatpak run io.github.todevelopers.GseProfilerThe remote is GPG-signed and the key is embedded in the .flatpakrepo
file, so no --no-gpg-verify is needed.
Want early test builds (rc/beta)? Add the testing remote instead:
https://todevelopers.github.io/flatpaks/todevelopers-testing.flatpakrepo
Prefer a one-off install without adding a remote? Grab the .flatpak
bundle from the
latest release
and install it (you won't get automatic updates this way):
flatpak install --user gseprofiler-*.flatpak
flatpak run io.github.todevelopers.GseProfilercurl -fsSL https://raw.githubusercontent.com/todevelopers/gseprofiler/main/scripts/setup-and-run.sh | bashThe script checks for GTK4 / libadwaita, clones the repo to
~/gse-profiler and launches the app. No sudo, no prompts. Run the same
command again later and it pulls the latest changes and restarts the app.
curl -fsSL https://raw.githubusercontent.com/todevelopers/gseprofiler/main/scripts/uninstall.sh | bashRemoves the app, desktop entry, icon, and bridge extension. Nothing else on your system is touched.
- GNOME Shell 46+ (tested up to 50)
- Python 3.11+
- GTK 4 and libadwaita 1
- PyGObject (GTK4 bindings)
python3-systemd, needed for the log viewer (systemd.journal.Reader); bundled automatically in the Flatpak
gse-profiler/
βββ app/ # GTK4 Python application
β βββ main.py
β βββ ui/
β β βββ extension_manager.py
β β βββ extension_list.py
β β βββ details_view.py
β β βββ log_viewer.py
β β βββ profiler_view.py
β β βββ profiler/ # flamegraph, swimlane, histogram widgets
β β βββ inspector_view.py
β βββ core/
β βββ dbus_client.py # D-Bus proxy for gnome-shell APIs
β βββ socket_server.py # Unix socket server (async)
β βββ bridge_manager.py # bridge install / update / hash check
β βββ journal_reader.py # systemd journal reader (systemd.journal.Reader)
βββ bridge-extension/ # GJS GNOME Shell extension
β βββ extension.js
β βββ profiler.js
β βββ inspector.js
β βββ socket_client.js
β βββ metadata.json
βββ build-aux/ # Flatpak manifest and launcher
βββ data/ # .desktop, AppStream metainfo, icons
βββ docs/ # architecture diagrams
βββ scripts/ # setup / uninstall helpers
βββ tests/ # pytest unit tests
See CONTRIBUTING.md for the full guide: development setup, local checks, scripts and CI/release automation.
If you find GSE Profiler useful or it saved you some time during debugging, consider supporting it on Ko-fi.
GPL-3.0-or-later. See LICENSE.




