Skip to content

Indicator takes about 2 seconds to open when many calendars are connected #332

Description

@peteruithoven

What Happened?

I'm afraid that since I connected my calendars (two CalDAV accounts, about 25 calendars and 5 task lists), opening the date & time indicator has become quite slow. It takes about 2 seconds before the indicator is usable and my events show up, every time I open it. It's on a fast machine (Ryzen AI 9 HX 370, 24 threads), so I suspect it's worse on more modest hardware.

I tried to find out what was going on and I think I found the cause. I hope this saves you some digging. Please correct me if I've misunderstood something.

Every open re-queries every calendar about 5 times.

Indicator.opened () calls CalendarView.refresh () → show_today (true), which rebuilds the carousel (introduced in #280 to fix #278). To build the previous and next month grids, it temporarily moves the shared CalendarModel around:

events_model.month_start = start_month;  // reload
events_model.change_month (-1);          // reload
events_model.change_month (2);           // reload
events_model.change_month (-1);          // reload

The same happens for tasks_model. After that, carousel.scroll_to () fires page_changed, which calls change_month (0), which reloads again. Every change of month_start runs on_parameter_changed () → load_all_sources (), which creates a new ECal.ClientView for every source. Usually the month hasn't changed at all since the last open, so the existing views already hold up-to-date data.

What I measured during a single open (right after restarting wingpanel):

  • 150 GetView + Start + Dispose calls to evolution-data-server, where 30 (one per source) would be enough. Also 52 synchronous GetObject calls, which I believe come from generate_instances_for_object_sync ().
  • evolution-calendar-factory used about 5 seconds of CPU, spread over 4–5 cores for about 1.4 s.
  • After that, wingpanel's main thread was 100% busy for about 0.5 s processing the results.
  • Total: about 1.9 s from opening until everything had settled.

This is closely related to #235, which describes the same model juggling when switching months, and it probably also explains #297, where the indicator flashes empty on open because the component maps are cleared and refilled.

Steps to Reproduce

  1. Connect one or more online calendar accounts with a good number of calendars (I have about 25 calendars and 5 task lists).
  2. Click the date & time indicator in the panel.
  3. Notice that it takes about 2 seconds before the indicator is usable and events show up. This happens on every open, not just the first.

To see the redundant queries:

dbus-monitor --session "type='method_call',path_namespace='/org/gnome/evolution/dataserver'" | grep -oE 'member=[A-Za-z]+'

Then open the indicator (e.g. io.elementary.wingpanel -o datetime) and count the GetView calls. I get 150 per open with 30 sources.

Expected Behavior

The indicator opens instantly and shows events right away, since the data for the current month is already loaded and kept up to date by the existing client views. Calendar queries should only happen when the visible month actually changes, and then once per source.

Suggested fix

I haven't been able to build and test this myself yet, so please treat it as a suggestion. With the three changes below, I'd expect a normal open to make no calendar queries at all, and a month change to make one query per source instead of four or five.

1. Compute a month's range without touching the model

In CalendarModel.vala, move the body of compute_ranges () into a public static function that takes its inputs as parameters:

public static Util.DateRange get_range_for_month (GLib.DateTime month_start, GLib.DateWeekday week_starts_on) {
    // …current body of compute_ranges (), using the parameters instead of the properties…
    return new Util.DateRange (data_range_first, data_range_last);
}

private void compute_ranges () {
    data_range = get_range_for_month (month_start, week_starts_on);
    num_weeks = data_range.to_list ().size / 7;
}

In CalendarView.vala, add a helper that builds a grid for any month without changing the model:

private DateTime.Widgets.Grid create_grid_for_month (GLib.DateTime month_start) {
    var range = CalendarModel.get_range_for_month (month_start, events_model.week_starts_on);

    var grid = create_grid ();
    grid.set_range (range, month_start);
    grid.update_weeks (range.first_dt, range.to_list ().size / 7);

    return grid;
}

This replaces every change_month (±1) → create_grid () → set_range () → update_weeks () → change_month (∓1) block. There are three of these: in construct, in show_today () and in page_changed. For example:

start_month_grid = create_grid_for_month (events_model.month_start);
var left_grid = create_grid_for_month (events_model.month_start.add_months (-1));
var right_grid = create_grid_for_month (events_model.month_start.add_months (1));

and in page_changed:

carousel.append (create_grid_for_month (events_model.month_start.add_months (1)));
// …
carousel.prepend (create_grid_for_month (events_model.month_start.add_months (-1)));

2. Don't reload when the range didn't actually change

In CalendarModel.vala, only reload when the range is different. This covers month_start = <same month> and change_month (0):

private void on_parameter_changed () {
    var old_range = data_range;
    compute_ranges ();

    if (old_range != null &&
        old_range.first_dt.equal (data_range.first_dt) &&
        old_range.last_dt.equal (data_range.last_dt)) {
        return;
    }

    load_all_sources ();
}

(I used .equal () here because DateRange.equals () compares the DateTimes with ==.)

3. Fill new grids with the event dots the model already has

Right now, grids only get their dots from the components_added signal, which fires because of the reload. Once opening no longer triggers a reload, the rebuilt grids need to take the dots from the data that's already loaded. At the end of Grid.set_range ():

events_model.source_components.@foreach ((source, components) => {
    add_component_dots (source, components.get_values ());
});
tasks_model.source_components.@foreach ((source, components) => {
    add_component_dots (source, components.get_values ());
});

GridDay.add_component_dot () already skips UIDs it has seen, so calling this more than once is harmless.

This keeps the carousel rebuild from #280 intact, so #278 shouldn't come back.

Possible follow-up (not needed for the open time)

page_changed still calls change_month (-rel_postion) followed by change_month (rel_postion), which is two reloads per swipe. Setting the month once, e.g. events_model.month_start = start_month.add_months (rel_postion), would halve that, which would help with #235.

Let me know if I can help debugging this in any way.

OS Version

8.x (Circe)

OS Architecture

amd64 (on most hardwares)

Session Type

Secure Session (Wayland, This is the default)

Software Version

Latest release (I have run all updates)

Log Output

# D-Bus calls from wingpanel to evolution-calendar-factory during one open
    150 member=GetView
    150 member=Start
    150 member=Dispose
     52 member=GetObject

# CPU usage per 100 ms after opening (ticks of 10 ms; eds = evolution-calendar-factory, all threads)
  235ms eds+48  wp_main+1
  576ms eds+58  wp_main+0
 1029ms eds+44  wp_main+0
 1367ms eds+44  wp_main+0
 1480ms eds+6   wp_main+10
 1708ms eds+2   wp_main+10
 1822ms eds+0   wp_main+6
 1935ms eds+0   wp_main+1   <- settled


Package versions: 
- wingpanel-indicator-datetime 2.4.2+r1007+pkg34~ubuntu8.1
- io.elementary.wingpanel 8.0.4+r765+pkg75~ubuntu8.1

Hardware Info

AMD Ryzen AI 9 HX 370 w/ Radeon 890M (24 threads), 64 GB RAM.
Two CalDAV (WebDAV) accounts with about 25 calendars and 5 task lists.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions