1

I have a database catalog that is populated, and a cursor that can be used to retrieve objects. This catalog can obviously be very large, and what I'd like to do is use ReactiveUI to buffer the data in, while keeping the UI data-bound and responsive. I followed the steps here to translate my IEnumerable into an IObservable, as shown here:

public class CatalogService
{
   ...

   public IObservable<DbObject> DataSource
   {
        get
        {
            return Observable.Create<DbObject>(obs =>
            {
                var cursor = Database.Instance.GetAllObjects();
                var status = cursor.MoveToFirst();

                while (status == DbStatus.OK)
                {
                    var dbObject= Db.Create(cursor);
                    obs.OnNext(dbObject);

                    status = cursor.MoveToNext();
                }

                obs.OnCompleted();

                return Disposable.Empty;
            });
        }
    }
}

In my view class (specifically, the Loaded event), I am subscribing to the data-source and using the buffer method in hopes of keeping the UI responsive.

    public ObservableCollection<DbObject> DbObjects { get; set; }

    private async void OnLoad(object sender, RoutedEventArgs e)
    {
        var observableData = CatalogService.Instance.DataSource.Publish();
        var chunked = observableData.Buffer(TimeSpan.FromMilliseconds(100));
        var dispatcherObs = chunked.ObserveOnDispatcher(DispatcherPriority.Background);
        dispatcherObs.Subscribe(dbObjects =>
        {
            foreach (var dbObject in dbObjects)
            {
                DbObjects.Add(dbObject);
            }
        });

        await Task.Run(() => observableData.Connect());
        await dispatcherObs.ToTask();
    }

The result is unfortunately quite the opposite. When my view control (which contains a simple ListBox data-bound to the DbObjects property) loads, it does not show any data until the entire catalog has been enumerated. Only then does the UI refresh.

I am new to ReactiveUI, but I am sure that it is capable for the task at hand. Does anyone have any suggestions or pointers if I am using it incorrectly?

Charlie
  • 15,069
  • 3
  • 64
  • 70
  • Questions: How have you established that the UI is not just simply rendering the results so fast you don't get to see them? Is the UI actually unresponsive? What is the time to first result and time to last result in the DB query? How many rows in your result set? – James World Nov 16 '13 at 01:49
  • Quick question... What object here is IEnumerable? – cwharris Nov 16 '13 at 05:49
  • @Christopher: Nothing is IEnumerable. The DataSource property was translated from an IEnumerable to an IObservable. – Charlie Nov 16 '13 at 05:53
  • Sorry, ment to say IEnumerable. – cwharris Nov 16 '13 at 05:53
  • @James: The UI is not actually unresponsive, but the ListBox is empty the entire time. There are around a thousand rows (images), and the ListBox goes from 0 images to all of them after a substantial amount of time. – Charlie Nov 16 '13 at 05:54
  • I must admit, I am baffled at the moment, because I can't make a simple listbox with bound images exhibit this behaviour. I can chuck 1000s in with no issue. Presumably, you have a byte array for the image data in your DBObject? What does your xaml look like, how are you databinding and how are you translating the bytes to an image? – James World Nov 17 '13 at 11:18

2 Answers2

2

Pending further information, my guess is you've got possibly several zero-length buffers depending on how long the DB query takes, followed by exactly one non-zero length buffer containing all the results. You're probably better off limiting buffer size by length as well as time.

EDIT - I just wanted to do an analysis of the various threads involved in the original implementation. I don't agree with Paul's analysis, I do not believe the UI Thread is blocked because of the DB query. I believe it's blocked due to large numbers of results being buffered.

Charlie - please, can you time the DB query in code (not with the debugger) and dump the buffer lengths you are getting too.

I will annotate the code to show the order of all three threads involved:

First of all, outside of the provided code, I am assuming a call is made to OnLoad via the Loaded event.

(1) - UI Thread calls OnLoad

public ObservableCollection<DbObject> DbObjects { get; set; }

private async void OnLoad(object sender, RoutedEventArgs e)
{
    // (2) UI Thread enters OnLoad

    var observableData = CatalogService.Instance.DataSource.Publish();

    var chunked = observableData
        // (6) Thread A OnNext passes into Buffer
        .Buffer(TimeSpan.FromMilliseconds(100));
        // (7) Thread B, threadpool thread used by Buffer to run timer 

    var dispatcherObs = chunked
        // (8) Thread B still
        .ObserveOnDispatcher(DispatcherPriority.Background);
        // (9) Non blocking OnNexts back to UI Thread

    dispatcherObs.Subscribe(dbObjects =>
    {
        // (10) UI Thread receives buffered dbObjects            
        foreach (var dbObject in dbObjects)
        {
            // (11) UI Thread hurting while all these images are
            // stuffed in the collection in one go - This is the issue I bet.
            DbObjects.Add(dbObject);
        }
    });

    await Task.Run(() =>
    {
        // (3) Thread A - a threadpool thread,
        // triggers subscription to DataSource
        // UI Thread is *NOT BLOCKED* due to await
        observableData.Connect()
    });
    // (13) UI Thread - Dispatcher call back here at end of Create call
    // BUT UI THREAD WAS NOT BLOCKED!!!

    // (14) UI Thread - This task will be already completed
    // It is causing a second subscription to the already completed published observable
    await dispatcherObs.ToTask();


}

public class CatalogService
{
   ...

   public IObservable<DbObject> DataSource
   {
        get
        {
            return Observable.Create<DbObject>(obs =>
            {
                // (4) Thread A runs Database query synchronously
                var cursor = Database.Instance.GetAllObjects();
                var status = cursor.MoveToFirst();

                while (status == DbStatus.OK)
                {
                    var dbObject= Db.Create(cursor);
                    // (5) Thread A call OnNext
                    obs.OnNext(dbObject);

                    status = cursor.MoveToNext();
                }

                obs.OnCompleted();
                // (12) Thread A finally completes subscription due to Connect()
                return Disposable.Empty;
            });
        }
    }
}

I think the issue is a large buffer unloading tons of results into the ObservableCollection in one go, creating a ton of work for the listbox.

James World
  • 29,019
  • 9
  • 86
  • 120
  • Tried limiting the number of items, all the way down to a mere 10 items. Same result-- the ListBox has no data for a long time, then eventually all of the items appear. – Charlie Nov 16 '13 at 05:52
  • I think James' theory still stands. The DB query is going to take a significant amount of time in comparison to iterating the results of the query. It's possible the delay you're experiencing simply the execution time of the query. Unless of course the results are streaming, and there are a significant amount of rows... – cwharris Nov 16 '13 at 05:55
  • Actually, I stepped through the query in the debugger and verified that it takes less than a second to execute. Remember, this is a database cursor, not a complete result. I am "moving" the cursor one element at a time, so this is not a very CPU-intensive operation. My theory is actually that the UI thread is blocked for some reason, but I can't see how. – Charlie Nov 16 '13 at 05:57
  • Or maybe not so much the UI thread as the data-binding operation. – Charlie Nov 16 '13 at 05:59
  • So here's an idea, but I could be completely off basis, because I typically don't use Rx.NET for GUI related stuff... You're observing on the dispatcher, but you're still subscribing on the current thread, which I believe might be causing the DB iterations to run synchronously. Could this be causing the problem? – cwharris Nov 16 '13 at 06:08
  • A reasonable idea, but the Publish method returns an IConnectableObservable, which will not actually push any results until the Connect method is called. And when I do the Connect, I make sure to put that on a background thread. – Charlie Nov 16 '13 at 06:11
  • Added more detail to my view of what is happening. – James World Nov 17 '13 at 01:02
  • Replace the timed buffer with a fixed size of 2 or 3... what happens then? – James World Nov 17 '13 at 01:26
  • You were right, James. The problem is the time that the first query takes (which does a lot of work). By the time that finishes, the items are ready to go and the buffering has no effect. Thanks for the carefully constructed analysis! – Charlie Nov 18 '13 at 05:06
0

Your problem is here:

           while (status == DbStatus.OK)
            {
                var dbObject= Db.Create(cursor);
                obs.OnNext(dbObject);

                status = cursor.MoveToNext();
            }

That loop runs synchronously as soon as someone subscribes, in a blocking way. Since you're creating the subscription on the UI thread (at the time you call Connect), it will run the entire thing on the UI thread. Change it to:

return Observable.Create<DbObject>(obs =>
{
    Observable.Start(() => {
        var cursor = Database.Instance.GetAllObjects();
        var status = cursor.MoveToFirst();

        while (status == DbStatus.OK)
        {
            var dbObject= Db.Create(cursor);
            obs.OnNext(dbObject);

            status = cursor.MoveToNext();
        }

        obs.OnCompleted();
    }, RxApp.TaskPoolScheduler);

    return Disposable.Empty;
});
Ana Betts
  • 73,868
  • 16
  • 141
  • 209
  • Hey Paul, thanks for the answer! I've actually been reading a lot of your stuff recently. Unfortunately this change doesn't help-- the result is that on start-up, I don't receive any results for about the first 10 seconds (strange, considering the 100 ms buffer?). Afterwards, I get all of the results at once, in whatever bucket size I specify in Buffer. – Charlie Nov 17 '13 at 00:12
  • After giving it some more thought, I think the problem is in querying for all of the objects at once; I think what I should instead do is only get the objects within the current viewport (e.g., the visible area of the window), and as the user scrolls, increment the starting index but still only retrieve a small subset of the data (say, 20 items). I think I could still accomplish this declaratively with ReactiveUI, but still having a bit of trouble setting it up. :( – Charlie Nov 17 '13 at 00:16
  • Pauls idea was my first thought - see my analysis for why there is no blocking of the UI thread like this. – James World Nov 17 '13 at 00:45