18

I'm not able to get ExtendedExecution to work properly. The problem is that the Revoked event is not being fired until the execution is finished. If we take a sample:

private async void OnSuspending(object sender, SuspendingEventArgs e)
{
    Debug.WriteLine("Suspending in app");
    var deferral = e.SuspendingOperation.GetDeferral();
    using (var session = new ExtendedExecutionSession())
    {
        session.Reason = ExtendedExecutionReason.SavingData;
        session.Description = "Upload Data";
        session.Revoked += (s, a) => { Debug.WriteLine($"Extended execution revoked because of {a.Reason}"); };
        var result = await session.RequestExtensionAsync();
        if (result == ExtendedExecutionResult.Denied) Debug.WriteLine("Extended execution failed");
        else
        {
            Debug.WriteLine("Executing");
            await Task.Run(() => { Task.Delay(9000).Wait(); Debug.WriteLine("Finished the task"); });
            Debug.WriteLine("Executing finished");
        }
        Debug.WriteLine("Suspending after execution");
    }
    deferral.Complete();
}

The documentation states that Revoked event should be fired upon resuming the app, but if you try the code with debugger attached, then you will see that debug output seems to look ok, but you have to wait 9000 ms for it to show up. This means that the code is suspended until the session finishes.

The biggest problem is that if you fire this without debugger attached, launch the app, suspend and then resume, you will see a black screen for few seconds and then OS will terminate your app.

Am I missing something? Has anybody got it working correctly?

Bassie
  • 9,529
  • 8
  • 68
  • 159
Romasz
  • 29,662
  • 13
  • 79
  • 154
  • Seems like that you are not missing anything but I cannot find anywhere in documentation that says "Revoked event should be fired upon resuming the app". Maybe you need to put some extra code in OnResuming instead. – Mehrzad Chehraz Mar 22 '16 at 22:12
  • @MehrzadChehraz No - the resuming event seems to be fired only after the suspending event is finished. This means that in release mode the app will be killed by the OS due to some time limits and won't rise revoked and resuming events at all. – Romasz Mar 22 '16 at 22:14
  • Yes you are right. By the way, can you add a link or something to the documentation that says "Revoked event should be fired upon resuming the app"? – Mehrzad Chehraz Mar 22 '16 at 22:18
  • @MehrzadChehraz I take it [from here at MSDN](https://msdn.microsoft.com/en-us/library/windows/apps/xaml/windows.applicationmodel.extendedexecution.extendedexecutionrevokedreason.aspx) - one of the two resons is app resumed, which makes sense - for example to pass cancellation signal, or fire different resuming method. As I've tested the above code, the order of debug output is correct - if you fire suspend and resume right away, wait few second, you will see that revoked event is fired before 'Executing finished'. Problem is that somehow you will see the output only after everything finishes. – Romasz Mar 22 '16 at 22:26
  • @MehrzadChehraz In release mode when PLM is disabled, the app doesn't have so much time so it will be terminated. I can just suspect that this is because UI thread is busy by awaiting in extended execution session. But IMHO, this doesn't make sense to use *ExtendedExecutionSession* in this case at all - if there is a big chance that the app will crash once the user gets back to it - more over you cannot say at what moment it will crash - for example in the middle of saving the file. Therefore I looking for things that I've missed, maybe just my thinking is wrong. – Romasz Mar 22 '16 at 22:33
  • I was thinking the same about UI Thread but I don't think that's the case. If it's gonna wait for something, then what's the point of the event? The only think that we miss is a reliable platform to work with. I ended up with not relying on OnSuspending at all. – Mehrzad Chehraz Mar 22 '16 at 22:39
  • @MehrzadChehraz As I think, the suspending event is one of the most important ones - I can't imagine how my app would work without such event - to send cancellation signal, finish some saving and more. – Romasz Mar 22 '16 at 22:41
  • I know, it's ridiculous, saving data should be done in intervals while app is running, I think a lightweight cancel signal (less than 5 secs) should be OK to be in suspending though. – Mehrzad Chehraz Mar 22 '16 at 22:47
  • Possible issue: You don't complete the deferral from `Suspending` until after everything is done. Can you complete as soon as you get the extended execution (before doing the Wait)? – Peter Torr - MSFT Mar 30 '16 at 00:31
  • @PeterTorr-MSFT As I've tested it helps with one thing - the app is resuming and not crashing without debugger. But there are couple of problems - the revoked event is not being called at all in this case and there is a danger that user suspends second time, then the app throws exception. Technically I can probably pass cancellation signal from resuming event, but I'm not sure about this - there are some resources probably freed after deferal.Complete() (I guess no revoked event comes from this) and thus I can't say if I will have the same reference to cancellation token upon resuming. – Romasz Mar 30 '16 at 04:58

2 Answers2

1

The usage of the await and Task is causing that your continued Task remains on the main thread making you have to wait with the black screen. Remember that the await behavior is to schedule the execution into the Dispatcher, NOT to begin a new thread, NOR schedule its execution into the ThreadPool. As consequence, No more UI messages can be processed until the Delay() ends.

Just perform your time-consuming operation on a new thread, but ensure to keep the session open until it ends.

Take a look of this https://msdn.microsoft.com/en-us/magazine/jj991977.aspx to get a good insight about how execution is sheduled

  • Any ideas how to perform time-consuming operation on a new thread and ensure to keep the session open until it ends, without using *await* on UI thread? – Romasz Mar 30 '16 at 04:28
1

There is no UI problem or anything. Your code works. Your expectation is wrong.

Using the ExtendedExecutionSession you're telling your app you need time to save and it shall not be revoked until you are finished. In your case that takes about 9 seconds.

Try suspending the app, wait for 10 seconds an then revoke it. It will happen immediately. Then try suspending the app and revoke it before the session is finished. Now the ExtendedExecutionSession will tell your OS that your app cannot be revoked yet and it has to wait until the saving process is finished. That's what you want.

See Microsoft doc on extended execution:

Requesting a ExtendedExecutionReason.SavingData extended execution session while the app is in the Suspending state creates a potential issue that you should be aware of. If an extended execution session is requested while in the Suspending state, and the user requests the app be launched again, it may appear to take a long time to launch. This is because the extended execution session time period must complete before the old instance of the app can be closed and a new instance of the app can be launched. Launch performance time is sacrificed in order to guarantee that user state is not lost.

What's mentioned in the section about "Revoke" is interesting for you, too:

When the Revoked event is fired for an ExtendedExecutionReason.SavingData extended execution session, the app has one second to complete the operation it was performing and finish Suspending.

One second is not enough to finish your 9 seconds waiting.

To eliminate the possibility of delayed display of your debug output, you can test it with adding the current time to the output. The OS probably has a problem with the session not closing properly because the 9 seconds don't finish.

Also note the remark on EnterBackground:

Previously your suspending callback was the best place to save state after a user finished a session with your app. However, now an application may continue running in the background and then move back to the foreground due to trigger activity without ever reaching the suspended state. The best place to save data after a user session is in your entered background event handler.

You will probably want to do your code in case the Exiting event is fired.

For OnSuspending try doing the waiting with a for loop that breaks (cancelling the saving process) as soon as the revoke happens, only waiting for half a second at a time.

UPDATE:

...Or use a Background Task, as suspending seems to be the only reliable warning before termination:

//
// Declare that your background task's Run method makes asynchronous calls by
// using the async keyword.
//
public async void Run(IBackgroundTaskInstance taskInstance)
{
    //
    // Create the deferral by requesting it from the task instance.
    //
    BackgroundTaskDeferral deferral = taskInstance.GetDeferral();

    //
    // Call asynchronous method(s) using the await keyword.
    //
    var result = await ExampleMethodAsync();

    //
    // Once the asynchronous method(s) are done, close the deferral.
    //
    deferral.Complete();
}

UPDATE2:

For the "proper" way how it should be done, see the official example:

private async void OnSuspending(object sender, SuspendingEventArgs args)
{
    suspendDeferral = args.SuspendingOperation.GetDeferral();

    rootPage.NotifyUser("", NotifyType.StatusMessage);

    using (var session = new ExtendedExecutionSession())
    {
        session.Reason = ExtendedExecutionReason.SavingData;
        session.Description = "Pretending to save data to slow storage.";
        session.Revoked += ExtendedExecutionSessionRevoked;

        ExtendedExecutionResult result = await session.RequestExtensionAsync();
        switch (result)
        {
            case ExtendedExecutionResult.Allowed:
                // We can perform a longer save operation (e.g., upload to the cloud).
                try
                {
                    MainPage.DisplayToast("Performing a long save operation.");
                    cancellationTokenSource = new CancellationTokenSource();
                    await Task.Delay(TimeSpan.FromSeconds(10), cancellationTokenSource.Token);
                    MainPage.DisplayToast("Still saving.");
                    await Task.Delay(TimeSpan.FromSeconds(10), cancellationTokenSource.Token);
                    MainPage.DisplayToast("Long save complete.");
                }
                catch (TaskCanceledException) { }
                break;
            default:
            case ExtendedExecutionResult.Denied:
                // We must perform a fast save operation.
                MainPage.DisplayToast("Performing a fast save operation.");
                await Task.Delay(TimeSpan.FromSeconds(1));
                MainPage.DisplayToast("Fast save complete.");
                break;
        }

        session.Revoked -= ExtendedExecutionSessionRevoked;
    }

    suspendDeferral?.Complete();
    suspendDeferral = null;
}

private async void ExtendedExecutionSessionRevoked(object sender, ExtendedExecutionRevokedEventArgs args)
{
    //If session is revoked, make the OnSuspending event handler stop or the application will be terminated
    if (cancellationTokenSource != null){ cancellationTokenSource.Cancel(); }

    await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
    {
        switch (args.Reason)
        {
            case ExtendedExecutionRevokedReason.Resumed:
                // A resumed app has returned to the foreground
                rootPage.NotifyUser("Extended execution revoked due to returning to foreground.", NotifyType.StatusMessage);
                break;

            case ExtendedExecutionRevokedReason.SystemPolicy:
                //An app can be in the foreground or background when a revocation due to system policy occurs
                MainPage.DisplayToast("Extended execution revoked due to system policy.");
                rootPage.NotifyUser("Extended execution revoked due to system policy.", NotifyType.StatusMessage);
                break;
        }

        suspendDeferral?.Complete();
        suspendDeferral = null;
    });
}
Neepsnikeep
  • 309
  • 3
  • 11
  • I still don't see the usage of Revoked for me - for now I can use Resuming, LeavingBackground, Launching method. I would expect the revoked event to run on separate thread, so I can pass cancellation tokens to my main task or do something. For now it runs on the same thread and is blocked untill extended execution finishes. – Romasz Aug 10 '17 at 20:38
  • That means that you have to use the extended execution on a different thread and not block the Suspending method.As Suspending expects you to finish in less than a few seconds and you need more time, I think this might help. I'd start a backround task from Suspending and use extended execution there. That way, it will always (try) finish saving no matter if you revoke your app or not during the saving process. If the data became corrupted when the app was running, you should abort the saving process in Revoked. – Neepsnikeep Aug 17 '17 at 07:18
  • Background Task is a different manner and it has nothing to Extended execution, as I think. Take a look that already my sample runs on a new thread, what shouldn't block the normal UI from work. – Romasz Aug 17 '17 at 07:54
  • You're right that it should not block your UI thread. But it's blocking the thread where the suspending and revoked events are triggered. Thus your problem that revoked is waiting for suspending to finish. So I suggest you run a background task and do not block suspending so that revoked event can fire as expected. I guess you're right that you don't need extended execution in the background thread... Ah yeah of course, you're right, extended execution is not what you want to happen as you expect your app to respond immediately no matter if the saving process is still running or not. – Neepsnikeep Aug 17 '17 at 08:12
  • That's why I think it's 'not working properly' - you are running something on background thread, expect to have responsive UI, but it's not, hence Suspending/Resuming is waiting for you background thread to finish, whereas it should just run normally on UI thread (as I think). – Romasz Aug 17 '17 at 08:32
  • But that's exactly what you're telling your app to do by extended execution. You tell your app that the suspended state is to be kept until your SavingData stuff is completed. No UI in suspended state. Thus your app does not respond until the 9 sec saving process is aborted because it's taking longer than the one second to finish. Even if your extended execution was not denied. – Neepsnikeep Aug 17 '17 at 08:56
  • Ok, but I would expect the *revoked* event to fire on unblocked UI thread and give possiblity to react somehow. Without this I really don't need this event (apart some very rare cases), hence everything is finished in suspending and I can use resuming event for everything else. – Romasz Aug 17 '17 at 09:03
  • That's why I suggested doing the extended execution session in a background task so that you can get the revoked event. What you can try is make your revoked event handler async and see if that's enough to be able to be executed on time. – Neepsnikeep Aug 17 '17 at 09:24
  • How would you put extended execution to background task? You mean the *IBackgroundTask*? (that's a different process) – Romasz Aug 17 '17 at 09:40
  • Yes, I mean registering a `IBackgroundTask` and thus avoiding the regular way of dealing with suspending and revoking the app. It's not the way it "should be done", but I do think it's what you want to happen. – Neepsnikeep Aug 17 '17 at 13:19