Using ArcGIS Maps SDK for .NET 200.5, I am having some issues with IdentifyLayersAsync and cancellation token.
I am working on a map with a few clustered feature layers, and at certain map scales the Identify operation will be very slow when identifying the clusters. This is particularly true when having just navigated the map before performing the identify operation, while DrawState is InProgress.
It appears that performing multiple sequential calls to this method are queued if user taps the view several times in a row and the previous Identify has not been completed. What happens next is that first call is blocking the next call, and the time the user has to wait increases for every new identify request.
I've used CancellationTokenSource and pass the token to the Identify method. When cancelling the token, the IdentifyLayersAsync does not appear to actually be cancelled until the last request has finished.
Looking at the source for GeoView, the request _coreReference.IdentifyLayers method does not use the cancellation token, but uses CoreTaskExtensions.AsTask to try to perform some cancellation that seems ineffective. As far as I can tell, Cancel registration does not work, is not called or doesn't end the native call.
I can see that AsTask is used in multiple locations of the API, and if it doesn't do the trick here, I'm guessing the same might be the case in other locations (assuming I've not missed something critical in my own code).
private async Task<IReadOnlyList<IdentifyLayerResult>> IdentifyLayersInternal(
Point screenPoint,
double tolerance,
bool returnPopupsOnly,
long maximumResultsPerLayer,
CancellationToken cancellationToken)
{
return (IReadOnlyList<IdentifyLayerResult>) new IdentifyLayerResultList(await this._coreReference.IdentifyLayers(this.TransformToDrawSurface(screenPoint), tolerance, returnPopupsOnly, (int) maximumResultsPerLayer).AsTask<CoreArray>(cancellationToken).ConfigureAwait(false));
}
This is the code to reproduce.
private int _identifyCounter;
private async void MapView_MapViewTapped(object sender, GeoViewInputEventArgs e)
{
var currentCounter = Interlocked.Increment(ref _identifyCounter);
if (_cancellationTokenSource != null)
{
await _cancellationTokenSource.CancelAsync();
_cancellationTokenSource.Dispose();
_cancellationTokenSource = null;
Trace.WriteLine($"Previous token CancelAsync", $"Tapped {currentCounter}");
}
CancellationTokenSource source = _cancellationTokenSource = new CancellationTokenSource();
var identifyWatch = Stopwatch.StartNew();
try
{
Trace.WriteLine("Start identify", $"OnIdentify {currentCounter}");
// Used when mapscale is less than 1 : 100.000
var maxResultPerLayer = 12;
var tolerance = 15;
await MapView.IdentifyGraphicsOverlaysAsync(e.Position, tolerance, false, maxResultPerLayer);
await MapView.IdentifyLayersAsync(e.Position, tolerance, returnPopupsOnly: true, maxResultPerLayer, source.Token);
identifyWatch.Stop();
}
catch (OperationCanceledException)
{
identifyWatch.Stop();
Trace.WriteLine($"OperationCanceledException after {identifyWatch.Elapsed}", $"Tapped {currentCounter}");
}
catch (Exception ex)
{
Trace.WriteLine(ex);
}
finally
{
if(!source.IsCancellationRequested)
{
Trace.WriteLine($"Finished after {identifyWatch.Elapsed}", $"Tapped {currentCounter}");
}
Interlocked.Decrement(ref _identifyCounter);
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
source?.Dispose();
}
}
The trace I get here is as follows:
Tapped 1: Finished after 00:00:00.1374115
OnIdentify 1: Start identify
Tapped 2: Previous token CancelAsync
OnIdentify 2: Start identify
Tapped 3: Previous token CancelAsync
OnIdentify 3: Start identify
Tapped 4: Previous token CancelAsync
OnIdentify 4: Start identify
Tapped 5: Previous token CancelAsync
OnIdentify 5: Start identify
Tapped 6: Previous token CancelAsync
OnIdentify 6: Start identify
Tapped 7: Previous token CancelAsync
OnIdentify 7: Start identify
Tapped 8: Previous token CancelAsync
OnIdentify 8: Start identify
Tapped 9: Previous token CancelAsync
OnIdentify 9: Start identify
Tapped 10: Previous token CancelAsync
OnIdentify 10: Start identify
Tapped 11: Previous token CancelAsync
OnIdentify 11: Start identify
Tapped 12: Previous token CancelAsync
OnIdentify 12: Start identify
Tapped 13: Previous token CancelAsync
OnIdentify 13: Start identify
Tapped 13: Finished after 00:00:12.8716381
Tapped 10: OperationCanceledException after 00:00:13.9292636
Tapped 11: OperationCanceledException after 00:00:13.6609924
Tapped 8: OperationCanceledException after 00:00:15.0679175
Tapped 2: OperationCanceledException after 00:00:18.1325766
Tapped 3: OperationCanceledException after 00:00:17.7673405
Tapped 9: OperationCanceledException after 00:00:14.6338782
Tapped 6: OperationCanceledException after 00:00:16.2799953
Tapped 12: OperationCanceledException after 00:00:13.5238723
Tapped 5: OperationCanceledException after 00:00:16.7167265
Tapped 4: OperationCanceledException after 00:00:17.4615539
Tapped 1: OperationCanceledException after 00:00:18.7209363
Tapped 7: OperationCanceledException after 00:00:15.8465043
If the cancellation worked properly, I would have expected the OperationCancelledException before each Start identify log statement, and the elapsed to not be insane 🙂
I will update the task with a sample webmap when my team has been able to extract the necessary data for public publishing, but hopefully there is enough information to go on here
Is there something that I am missing, or is there a bug going on here?
I've attached animation of the output of the trace. On bad tests this can accumulate up to an elapsed time of over a minute if I'm impatient enough to be aggressive with clicking
I can reproduce the issue in this application. Zoom in/out, click clusters/features with high frequency, and the "Concurrent identify count" on the screen will increase and the trace can be seen in the output window
This application can also reproduce an issue where the whole application deadlocks and remains frozen, unresponsive to any input, task manager termination or the debugger being able to pause to see where the freeze occurs.
In a different scenario where I had identify on mouse move, the above issue would occur when IdentifyLayer/IdentifyOverlay happens when DrawState was InProgress. For the tooltip I could disable the feature if map has not finished drawing.
For identify through click would confuse the user with "nothing happening", but is of course a work-around to keep the application from crashing/freezing.
The sample can also replicate a binding error in the Toolkit like this:
System.Windows.Data Error: 1 : Cannot create default converter to perform 'one-way' conversions between types 'Esri.ArcGISRuntime.Mapping.Popups.FieldsPopupElement' and 'Esri.ArcGISRuntime.Mapping.Popups.AttachmentsPopupElement'. Consider using Converter property of Binding.
System.Windows.Data Error: 5 : Value produced by BindingExpression is not valid for target property. FieldsPopupElement:'Esri.ArcGISRuntime.Mapping.Popups.FieldsPopupElement' BindingExpression:Path=; DataItem='FieldsPopupElement' (HashCode=35307962); target element is 'FieldsPopupElementView' (Name=''); target property is 'Element' (type 'AttachmentsPopupElement')
As a side note, I can occasionally get an NullReferenceException with the CancellationTokenSource if I'm extremely aggressive, though I don't think that is related to the root issue, though I'd be open to be proved wrong
I am working on a map with a few clustered feature layers, and at certain map scales the Identify operation will be very slow when identifying the clusters.
Hi @QuintenKent
Thank you for getting back to me. I've extended the sample a little bit.
New and old discoveries (see screenshot)
Updating both toolkit and SDK to 200.6 removes the binding error
diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs
index a510c9c..63a29aa 100644
--- a/MainWindow.xaml.cs
+++ b/MainWindow.xaml.cs
@@ -113,13 +113,12 @@ public partial class MainWindow : Window, INotifyPropertyChanged
if (!source.IsCancellationRequested)
{
Trace.WriteLine($"Finished after {identifyWatch.Elapsed}", $"Tapped {currentCounter}");
+ source.Dispose();
+ _cancellationTokenSource = null;
}
Interlocked.Decrement(ref _identifyCounter);
OnPropertyChanged(nameof(IdentifyCounter));
- _cancellationTokenSource?.Dispose();
- _cancellationTokenSource = null;
- source.Dispose();
}
}
diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs
index a510c9c..8d5e552 100644
--- a/MainWindow.xaml.cs
+++ b/MainWindow.xaml.cs
@@ -77,7 +77,7 @@ public partial class MainWindow : Window, INotifyPropertyChanged
OnPropertyChanged(nameof(IdentifyCounter));
if (_cancellationTokenSource != null)
{
- await _cancellationTokenSource.CancelAsync();
+ _cancellationTokenSource.Cancel();
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
Trace.WriteLine($"Previous token CancelAsync", $"Tapped {currentCounter}");
Finally, for the crash, would you be able to re-test with the changes to cancellation token source mentioned previously? I would be curious if they are related.
Thanks!
I've tried the cancellation token changes which seems to have some effect on the unprocessed queue.
Related to the crash, it occurs from time to time, but with two symptoms. The way to reproduce is to zoom the map so DrawState is InProgress and then use Control + mouse move to build the queue.
The symptoms:
It might beneficial to create a sample for the Tutorials section of the training materials for identify with cancellation token for others in the future. I've seen some samples where mouse move is used for map tips. Our real application has/had layers with crazy amount of data where it would take over a minute before the task was cancelled with only a few queued identify operations. The webmap was the minimum to reproduce the issue.