runceel / ReactiveProperty

ReactiveProperty provides MVVM and asynchronous support features under Reactive Extensions. Target frameworks are .NET 6+, .NET Framework 4.7.2 and .NET Standard 2.0.
MIT License
903 stars 101 forks source link

ToFilteredReadOnlyObservableCollectionをしているO.C.にAddすると時間がかかってしまう #237

Closed kenichi-kobayashi closed 3 years ago

kenichi-kobayashi commented 3 years ago

処理時間、メモリ共に、大幅の改善が行われたことを確認できました。 V7.8.0のリリースありがとうございます。

そして、更なる課題が出てきたので、相談させてください。

前回の実験では、Itemsに100万オブジェクトを生成した後に、ToFilteredを実行してIFlteredROOCを作成していました。

    public MyItemsViewModel()
    {
        Items = new ObservableCollection<MyItemViewModel>();
        Enumerable.Range(1, 1000000).Select(a => new MyItemViewModel(a)).ToList().ForEach(a => Items.Add(a));

        FilteredItems = Items.ToFilteredReadOnlyObservableCollection(item => item.Value % 2 == 0);
        Count = FilteredItems.CountAsObservable().ToReadOnlyReactivePropertySlim();
    }

Rpを7.8.0にすることで、このコードの処理時間が5秒程度になり実用可能レベルの処理時間になりました。 ただ、IFlteredROOCを実用する多くは場合、Itemsの生成と同時に生成するシーンが多いかと思います。

    public MyItemsViewModel2()
    {
        Items = new ObservableCollection<MyItemViewModel>();
        FilteredItems = Items.ToFilteredReadOnlyObservableCollection(item => item.Value % 2 == 0).AddTo(disposables);
        Count = FilteredItems.CountAsObservable().ToReadOnlyReactivePropertySlim();

        Enumerable.Range(1, 1000000).Select(a => new MyItemViewModel(a)).ToList().ForEach(a => Items.Add(a));
    }

これを実行すると、1時間以上の時間がかかってしまい、実用が難しいことが分かってきました。 これを回避するために、以下のとおりに対策したらどうか?と思っています。

■対策:アプリケーション側 ObservableCollectionを継承し、AddRange機能を装備した「RangedObservableCollection」を定義します。 AddRange実行中は、CollectionChangedイベントが発行されないように抑制しておき、登録終了後に、Resetイベントを発行するようにします。 参考URL:https://peteohanlon.wordpress.com/2008/10/22/bulk-loading-in-observablecollection/

■対策:ReactiveProperty側 ReactiveProperty側では、以下のとおりにResetの処理に細工を入れます。

public FilteredReadOnlyObservableCollection(TCollection source, Func<TElement, bool> filter) の source.CollectionChangedAsObservable() を以下のとおりに修正します。

                        case NotifyCollectionChangedAction.Reset:
                            Initialize();       // これを追加して
                            //IndexList.Clear();             // これらを外す。
                            //InnerCollection.Clear();  // これらを外す。
                            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
                            //if (source.Any())     // これらを外す。
                            //{             // これらを外す。
                            //    throw new NotSupportedException("Reset is clear only");   // これらを外す。
                            //}             // これらを外す。
                            break;

上記対策を実施したところ、以下のコードが5秒程度に収まり実用レベルになりました。

    public MyItemsViewModel3()
    {
        Items = new RangedObservableCollection<MyItemViewModel>();
        FilteredItems = Items.ToFilteredReadOnlyObservableCollection(item => item.Value % 2 == 0).AddTo(disposables);
        Count = FilteredItems.CountAsObservable().ToReadOnlyReactivePropertySlim();

        var items = Enumerable.Range(1, 1000000).Select(a => new MyItemViewModel(a)).ToList();
        Items.AddRange(items);  
    }

FilteredReadOnlyObservableCollection.zip

FilteredAddRangeTest.zip

runceel commented 3 years ago

対応を入れたものを 7.8.1 のプレリリース版としてリリースのパイプラインを走らせてみました。 確認お願いします。