I had a feeling that I’ve written about ExtendedExecutionSession before and I know that I spoke about it in a session that I did on background work in WinRT apps that I delivered back at TechDays in the Netherlands in 2015.
It took a little search through my blog to find that I had indeed written up something about it in a previous post;
and that included some topics like;
- the app lifecycle and why it means you need background work
- extended execution
- notifications
- background transfers
- background tasks
but the arrival of the Anniversary Update build 1607 makes some of that somewhat out of date in that there have been changes to many of these areas and I made reference to some of the links on these areas in this recent post;
Windows 10 1607, UWP, Single Process Execution and Lifecycle Changes
and so I won’t repeat those links here.
Today, though, I took some of the code that I’d shown in that TechDays session around “Extended Execution” and I brought it forward to the current 14393 UWP SDK and I found that it didn’t work and that surprised me and so I thought I would note that here.
As a quick re-cap: UWP apps are suspended (and possibly terminated) when the user is not actively using them and extended execution is a way to ask the system to take your app out of that suspend/resume process and you can use it in two ways;
- As a blanket ‘up front’ request that the system doesn’t suspend you. Naturally, the system can say ‘no’ and it can also change its mind at a later point.
- As a request from your ‘suspending’ event handler to let the system know that you need more time.
To experiment with this I made a simple, blank app with this super-simple UserControl as the UI;
<UserControl x:Class="App52.MyUIControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:App52" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400"> <Viewbox> <Grid Margin="4"> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <TextBlock TextAlignment="Center" x:Name="txtTime" /> <Button Grid.Row="1" HorizontalAlignment="Stretch" x:Name="btnExtended" Content="Go Extended" /> <Button Grid.Row="2" HorizontalAlignment="Stretch" x:Name="btnLeaveExtended" Content="Leave Extended" /> </Grid> </Viewbox> </UserControl>
and some code behind which changes the value of the TextBlock and which allows a caller to subscribe/unsubscribe event handlers to the 2 buttons;
namespace App52 { using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; public sealed partial class MyUIControl : UserControl { public MyUIControl() { this.InitializeComponent(); } public void SetText(string text) { this.txtTime.Text = text; } public void SetClearExtendedHandler(RoutedEventHandler handler, bool clear=false) { SetClearClickHandler(this.btnExtended, handler, clear); } public void SetClearUnextendedHandler(RoutedEventHandler handler, bool clear=false) { SetClearClickHandler(this.btnLeaveExtended, handler, clear); } static void SetClearClickHandler(Button button, RoutedEventHandler handler, bool clear) { if (!clear) { button.Click += handler; } else { button.Click -= handler; } } } }
and then I removed the MainPage.xaml/MainPage.xaml.cs files from my project and replaced the App.xaml.cs code with the below;
namespace App52 { using System; using Windows.ApplicationModel; using Windows.ApplicationModel.Activation; using Windows.ApplicationModel.ExtendedExecution; using Windows.UI.Xaml; sealed partial class App : Application { public App() { this.InitializeComponent(); this.EnteredBackground += OnEnteredBackground; this.LeavingBackground += OnLeavingBackground; this.Suspending += OnSuspending; this.startTime = DateTime.Now; this.timer = new DispatcherTimer() { Interval = TimeSpan.FromSeconds(1) }; this.timer.Tick += OnTimerTick; this.timer.Start(); } protected override void OnLaunched(LaunchActivatedEventArgs args) { Window.Current.Activate(); } void OnLeavingBackground(object sender, LeavingBackgroundEventArgs e) { this.isBackground = false; this.CreateUI(); } void OnEnteredBackground(object sender, EnteredBackgroundEventArgs e) { this.DestroyUI(); this.isBackground = true; } void OnTimerTick(object sender, object e) { if (this.isBackground) { this.backgroundTimeSeconds++; } else { this.regularTimeSeconds++; this.UpdateUI(); } } void UpdateUI() { var elapsed = (DateTime.Now - this.startTime).TotalSeconds; var total = this.regularTimeSeconds + this.backgroundTimeSeconds; // lazy, should use stringbuilder. this.uiControl.SetText( $"regular {this.regularTimeSeconds}\n" + $"background {this.backgroundTimeSeconds}\n" + $"total (r + b) {this.regularTimeSeconds + this.backgroundTimeSeconds}\n" + $"elapsed {elapsed:N0}\n" + $"missing {elapsed - total:N0}"); } void CreateUI() { this.uiControl = new MyUIControl(); this.uiControl.SetClearExtendedHandler(this.OnExtendedMode); this.uiControl.SetClearUnextendedHandler(this.OnUnextendedMode); Window.Current.Content = this.uiControl; } void DestroyUI() { this.uiControl.SetClearExtendedHandler(this.OnExtendedMode, true); this.uiControl.SetClearUnextendedHandler(this.OnUnextendedMode, true); this.uiControl = null; Window.Current.Content = null; } async void OnExtendedMode(object sender, RoutedEventArgs e) { if (this.extendedExecutionSession == null) { this.extendedExecutionSession = new ExtendedExecutionSession() { Reason = ExtendedExecutionReason.LocationTracking, Description = "locating tracking" }; this.extendedExecutionSession.Revoked += OnExtensionRevoked; var result = await this.extendedExecutionSession.RequestExtensionAsync(); if (result == ExtendedExecutionResult.Allowed) { // We're running extended. } else { // We're not. this.OnUnextendedMode(null, null); } } } void OnExtensionRevoked(object sender, ExtendedExecutionRevokedEventArgs args) { this.OnUnextendedMode(null, null); } void OnUnextendedMode(object sender, RoutedEventArgs e) { if (this.extendedExecutionSession != null) { this.extendedExecutionSession.Dispose(); this.extendedExecutionSession = null; } } void OnSuspending(object sender, SuspendingEventArgs e) { // deliberately nothing here. } ExtendedExecutionSession extendedExecutionSession; int regularTimeSeconds; int backgroundTimeSeconds; bool isBackground; MyUIControl uiControl; DispatcherTimer timer; DateTime startTime; } }
What’s this intended to do? It’s trying to keep a track of the number of seconds that a 1-second timer in the app is actively firing versus the total elapsed time that the app has been running. It’s also trying to partition those 1-second timer ticks into ‘foreground’ and ‘background’ categories.
It’s not intended to survive suspend/terminate/launch.
If I run this app outside of the debugger for ~10 seconds then I’m going to see output like;
Now, if I minimise the app at the 10 second mark and then maximise it around 20 seconds later I see something like;
Ok – no surprise. My app has been ‘alive’ for around 34 seconds. Of that, for 13 seconds my app was getting timer ticks in the foreground. For 6 seconds it was getting them in the background and then for 15 seconds it didn’t get any timer ticks because the app was suspended.
Now, what surprised me is that I then repeated my experiment after clicking the ‘Go Extended’ button to request ‘extended execution’ with the event handler running the same code that I’d been running back at TechDays a little more than a year ago. That is – this code;
async void OnExtendedMode(object sender, RoutedEventArgs e) { if (this.extendedExecutionSession == null) { this.extendedExecutionSession = new ExtendedExecutionSession() { Reason = ExtendedExecutionReason.LocationTracking, Description = "locating tracking" }; this.extendedExecutionSession.Revoked += OnExtensionRevoked; var result = await this.extendedExecutionSession.RequestExtensionAsync(); if (result == ExtendedExecutionResult.Allowed) { // We're running extended. } else { // We're not. this.OnUnextendedMode(null, null); } } }
Here’s what I saw after around 30 seconds repeating that experiment;
Hmmm – I’m still missing 15 seconds of timer ticks here. I seem to have not managed to go into extended mode and if I debug this I see;
which explains it. The question was then what to do about that and I wondered whether the previous ‘get out of jail’ of providing a reason of “location” was no longer valid for an app that’s not actually doing that. I changed that code;
this.extendedExecutionSession = new ExtendedExecutionSession() { Reason = ExtendedExecutionReason.Unspecified, // Change! Description = "general playing around" };
and, sure enough, repeating my experiment for around 30 seconds again gave me more like the result I was expecting;
i.e. pretty much no time lost to suspension when I’ve asked for extended execution so just recording that here in case someone else comes across it (including me in the future ).