Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add spec: Throttling Control - Script #4156

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
291 changes: 291 additions & 0 deletions specs/ThrottlingControlScript.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
Throttling Control - Script Throttling
===

# Background
Web content in WebView2 is generally subject to the same Web Platform
restrictions as in the Microsoft Edge browser. However, some of the scenarios
for WebView2 applications differ from the scenarios in the browser. For this
reason, we're providing a set of APIs to fine-tune performance of scripts
running in WebView2. These APIs allow WebView2 applications to achieve two
things:

* Customize script timers (`setTimeout` and `setInterval`) throttling under
different page states (foreground, background, and background with intensive
throttling)
* Throttle script timers in select hosted iframes

# Examples

## Throttle timers in visible WebView

Throttling Control APIs allow you to throttle JavaScript timers in scenarios
where the WebView2 control in your application needs to remain visible, but
consume less resources, for example, when the user is not interactive.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be helpful to elaborate a bit on what 'throttling' means in this case. If I set a throttling interval of 500ms, that means that timers will file at most once every 500ms - is that correct?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Especially because the Chromium docs call out two stages of throttling.

Stage 1: Timers run at normal speed for X seconds.
Stage 2: After X seconds, timers run at maximum speed Y.

It may not be clear whether this setting controls X or Y. I think it controls Y.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This paragraph is for API review only. Proper documentation for "throttling" meaning is in the API:

/// The preferred wake up interval (in milliseconds) to use for throttleable
/// JavaScript tasks (`setTimeout` and `setInterval`) [...]

/// A wake up interval is the amount of time that needs to pass before the
/// WebView2 Runtime checks for new timer tasks to run.

/// [...]
[propget] HRESULT ThrottlingIntervalPreferenceForeground([out, retval] UINT32* value);

I can use similar wording to the question above in the "For example" portion of the docs:

If I set a throttling interval of 500ms, that means that timers will file at most once every 500ms

/// For example, an application might use a foreground value of 30 ms for
/// moderate throttling scenarios, or match the default background value
/// (usually 1000 ms). **In this case, timers will file at most once every 30ms.**

Copy link
Contributor Author

@lflores-ms lflores-ms Nov 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two stages of throttling from the referred document map as follows:

  • X -> ThrottlingIntervalPreferenceForeground
  • Y -> ThrottlingIntervalPreferenceBackground
  • there's a third state: ThrottlingIntervalPreferenceIntensive

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update docs. See discussion later on for changing name to be clearer.


```c#
void OnNoUserInteraction()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like an event callback, but it is implied business logic of the sample app, not something rooted in the wv2 API surface, correct? Is there a prescribed way app should determine when the customer isn't "interactive"? (Or, is this concept already present in our samples elsewhere?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No prescribed way to implement detection logic, this is up to the app.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a comment to make it clear how the app would call this method.
A comment like 'the sample app calls this when ... but you might consider doing this in these cases ...'.
Follow up if we want to improve sample code to demonstrate how to actually do this - if we determine this is a common thing that will be helpful.

{
// User is not interactive, keep webview visible but throttle timers to 500ms.
webView.CoreWebView2.SetThrottlingIntervalPreference(CoreWebView2ThrottlingCategory.Foreground, 500);
}

void OnUserInteraction()
{
// User is interactive again, unthrottle foreground timers.
webView.CoreWebView2.SetThrottlingIntervalPreference(CoreWebView2ThrottlingCategory.Foreground, 0);
}
```

```cpp
void ScenarioThrottlingControl::OnNoUserInteraction()
{
auto webView21 = m_webview.try_query<ICoreWebView2_21>();
CHECK_FEATURE_RETURN_EMPTY(webView21);

// User is not interactive, keep webview visible but throttle timers to
// 500ms.
CHECK_FAILURE(webView21->SetThrottlingIntervalPreference(
COREWEBVIEW2_THROTTLING_CATEGORY_FOREGROUND, 500));
}

void ScenarioThrottlingControl::OnUserInteraction()
{
auto webView21 = m_webview.try_query<ICoreWebView2_21>();
CHECK_FEATURE_RETURN_EMPTY(webView21);

// User is interactive again, unthrottle foreground timers.
CHECK_FAILURE(webView21->SetThrottlingIntervalPreference(
COREWEBVIEW2_THROTTLING_CATEGORY_FOREGROUND, 0));
}
```

## Unthrottle timers in hidden WebView

Throttling Control APIs allow you to set a custom throttling interval for timers
on hidden WebViews. For example, if there's logic in your app that runs in
JavaScript but doesn't need to render content, you can keep the WebView hidden
and unthrottle its timers.

```C#
void SetupHiddenWebViewCore()
{
// This WebView2 will remain hidden but needs to keep running timers.
// Unthrottle background timers.
webView.CoreWebView2.SetThrottlingIntervalPreference(CoreWebView2ThrottlingCategory.Background, 0);
// Effectively disable intensive throttling by overriding its timer interval.
webView.CoreWebView2.SetThrottlingIntervalPreference(CoreWebView2ThrottlingCategory.Intensive, 0);
webView.Visibility = System.Windows.Visibility.Hidden;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want the throttling changes to take effect immediately, do we need to do a fake "navigate to self"? (Though that would destroy any page state, so maybe not?)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

n/a see above

Copy link

@sschalek sschalek Nov 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the hosted JS know that it is invisible but there is a desire for it to still run in this state? Or would this info have to come through a different channel, or is the expectation that hosted code will always try to run if it can and won't itself, say, stop regular updates or communication with its backing service if the web platform says it's not visible?

Another way of putting it is, how is "visible as far as the web platform is concerned" differentiated from "host app has set the WV2 element to be visible" from "content is actually visible" both from the perspective of the host app calling these APIs and the hosted code having access to the web platform APIs?

Copy link
Contributor Author

@lflores-ms lflores-ms Nov 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JavaScript context only has knowledge of "visible as far as the web platform is concerned" through Page Visibility API.

From the host app side, this is controlled by CoreWebView2Controller.IsVisible property. For WebView2 controls in .NET/WinRT, IsVisible property is controlled directly by the framework and tied to actual user visibility. The hosted JavaScript has no knowledge of this through its Web Platform API, but if needed, can be informed by the host through window.chrome.webview object.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding explicit end to end description of how API fits into chromium/web platform feature to the ref docs and examples of how an end dev might use it

}

void DisableHiddenWebViewCore()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method name is confusing. The name says that it's disabling the hidden webview, but really it's showing the webview and setting new values.

Is this code trying to restore defaults? If so, it should save the previous values of the properties in SetupHiddenWebViewCore so that it can restore them here, rather than hard-coding what it believes to be the defaults.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name is sort of double negative. Better would be Hide/ShowWebView
If we're trying to undo previous changes back to previous values, we should be saving those values or showing how to do this properly.

{
webView.Visibility = System.Windows.Visibility.Visible;
webView.CoreWebView2.SetThrottlingIntervalPreference(CoreWebView2ThrottlingCategory.Background, 1000);
webView.CoreWebView2.SetThrottlingIntervalPreference(CoreWebView2ThrottlingCategory.Intensive, 60000);
}
```

```cpp
void ScenarioThrottlingControl::SetupHiddenWebViewCore()
{
auto webView21 = m_webview.try_query<ICoreWebView2_21>();
CHECK_FEATURE_RETURN_EMPTY(webView21);

// This WebView2 will remain hidden but needs to keep running timers.
// Unthrottle background timers.
CHECK_FAILURE(webView21->SetThrottlingIntervalPreference(
COREWEBVIEW2_THROTTLING_CATEGORY_BACKGROUND, 0));
// Effectively disable intensive throttling by overriding its timer interval.
CHECK_FAILURE(webView21->SetThrottlingIntervalPreference(
COREWEBVIEW2_THROTTLING_CATEGORY_INTENSIVE, 0));

CHECK_FAILURE(m_appWindow->GetWebViewController()->put_IsVisible(FALSE));
}

void ScenarioThrottlingControl::DisableHiddenWebViewCore()
{
CHECK_FAILURE(m_appWindow->GetWebViewController()->put_IsVisible(TRUE));

auto webView21 = m_webview.try_query<ICoreWebView2_21>();
CHECK_FEATURE_RETURN_EMPTY(webView21);

CHECK_FAILURE(webView21->SetThrottlingIntervalPreference(
COREWEBVIEW2_THROTTLING_CATEGORY_BACKGROUND, 1000));
CHECK_FAILURE(webView21->SetThrottlingIntervalPreference(
COREWEBVIEW2_THROTTLING_CATEGORY_INTENSIVE, 60000));
}
```

## Throttle timers in hosted iframes

Throttling Control APIs allow you to throttle timers in specific frames within
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So every frame is either a 'regular' frame or an 'isolated' frame. Regular frames are controlled by ThrottlingIntervalPreferenceForeground/ThrottlingIntervalPreferenceBackground/ThrottlingIntervalPreferenceIntensive. Isolated frames are controlled by ThrottlingIntervalPreferenceIsolated.

Is there a reason why an isolated frame does not have Foreground/Background/Intensive properties? Or is that not an interesting scenario?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid question - Mostly not an interesting enough scenario to add the complexity. The primary use-case here is an app embedding 3rd party content and wanting to be able to independently limit the performance impact of it. Generally that's something like "low battery, throttle more" or "giving the frame N seconds to run some logic, throttle less".

The case where they'd want to put it in an isolated group while still managing foreground/background/intensive independently of the non-isolated timers would be a niche of a niche that even our most micromanage-y apps wouldn't want that granularity.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this value takes precedence over the other values?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update sample to set isolated to at least match background to help demonstrate intended scenario / usage of isolated. Use Andy's comment to update the sample code.

Other names to consider:

  • Custom
  • Alternate
  • Override

Use the following:
PreferredOverrideTimerWakeIntervalInMilliseconds
UseOverrideTimerWakeInterval

the WebView2 control. For example, if your application uses iframes to host 3rd
party content, you can select and mark these frames to be throttled separately
from the main frame and other regular, unmarked frames.

```C#
void SetupUntrustedFramesHandler()
{
webView.CoreWebView2.FrameCreated += (sender, args) =>
{
// You can use the frame properties to determine whether it should be
// marked to be throttled separately from main frame.
if (args.Frame.Name == "untrusted")
{
args.Frame.IsUntrusted = true;
}
};

webView.CoreWebView2.SetThrottlingIntervalPreference(CoreWebView2ThrottlingCategory.UntrustedFrame, 500);
}
```

```cpp
void ScenarioThrottlingControl::SetupUntrustedFramesHandler()
{
auto webview4 = m_webview.try_query<ICoreWebView2_4>();
CHECK_FEATURE_RETURN_EMPTY(webview4);

// You can use the frame properties to determine whether it should be
// marked to be throttled separately from main frame.
CHECK_FAILURE(webview4->add_FrameCreated(
Callback<ICoreWebView2FrameCreatedEventHandler>(
[this](ICoreWebView2* sender, ICoreWebView2FrameCreatedEventArgs* args) -> HRESULT
{
wil::com_ptr<ICoreWebView2Frame> webviewFrame;
CHECK_FAILURE(args->get_Frame(&webviewFrame));

auto webviewFrame6 =
webviewFrame.try_query<ICoreWebView2Frame6>();
CHECK_FEATURE_RETURN_HRESULT(webviewFrame6);

wil::unique_cotaskmem_string name;
CHECK_FAILURE(webviewFrame->get_Name(&name));
if (wcscmp(name.get(), L"untrusted") == 0)
{
CHECK_FAILURE(webviewFrame6->put_IsUntrusted(TRUE));
}

return S_OK;
})
.Get(),
&m_frameCreatedToken));

auto webView21 = m_webview.try_query<ICoreWebView2_21>();
CHECK_FAILURE(webView21->SetThrottlingIntervalPreference(
COREWEBVIEW2_THROTTLING_CATEGORY_UNTRUSTED_FRAME, 500));
}
```

# API Details
```cpp
[v1_enum]
typedef enum COREWEBVIEW2_THROTTLING_CATEGORY {
david-risney marked this conversation as resolved.
Show resolved Hide resolved
/// Applies to frames whose WebView is in foreground state. WebViews whose
david-risney marked this conversation as resolved.
Show resolved Hide resolved
/// `IsVisible` property is `TRUE` are in this state. The default value is a
/// constant determined by the running version of the WebView2 Runtime.
COREWEBVIEW2_THROTTLING_CATEGORY_FOREGROUND,

/// Applies to frames whose WebView is in background state. WebViews whose
/// `IsVisible` property is `FALSE` are in this state. The default value is a
/// constant determined by the running version of the WebView2 Runtime.
/// All other background state policies (including intensive throttling) are
/// effective independently of this setting.
COREWEBVIEW2_THROTTLING_CATEGORY_BACKGROUND,

/// Applies to frames whose WebView is being intensively throttled (a
david-risney marked this conversation as resolved.
Show resolved Hide resolved
/// sub-state of background state). For more details about intensive
/// throttling, see [Intensive throttling of Javascript timer wake ups](https://chromestatus.com/feature/4718288976216064).
/// The default value is a constant determined by the running version of the
/// WebView2 Runtime.
COREWEBVIEW2_THROTTLING_CATEGORY_INTENSIVE,

/// Applies to frames that have been marked untrusted by the host app.
/// This is a category specific to WebView2 with no corresponding state in the
/// Chromium tab state model. The default value is a constant determined by
/// the running version of the WebView2 Runtime.
COREWEBVIEW2_THROTTLING_CATEGORY_UNTRUSTED_FRAME
} COREWEBVIEW2_THROTTLING_CATEGORY;

/// A continuation of the `ICoreWebView2` interface to support ThrottlingPreference.
[uuid(00f1b5fb-91ed-4722-9404-e0f8fd1e6b0a), object, pointer_default(unique)]
interface ICoreWebView2_21 : ICoreWebView2_20 {
/// Get the preferred wake up interval (in milliseconds) for throttleable
/// JavaScript tasks (`setTimeout` and `setInterval`), for the given
david-risney marked this conversation as resolved.
Show resolved Hide resolved
/// throttling category. A wake up interval is the amount of time that needs
david-risney marked this conversation as resolved.
Show resolved Hide resolved
/// to pass before the WebView2 Runtime checks for new timer tasks to run.
/// The default interval values are constants determined by the running
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarified meaning of "determined by the WebView2 runtime".

/// version of the WebView2 Runtime.
HRESULT GetThrottlingIntervalPreference(
[in] COREWEBVIEW2_THROTTLING_CATEGORY category,
[out, retval] UINT32* intervalInMilliseconds);

/// Sets the preferred wake up interval (in milliseconds) for throttleable
/// JavaScript tasks (`setTimeout` and `setInterval`), for the given
/// throttling category. A wake up interval is the amount of time that needs
/// to pass before the WebView2 Runtime checks for new timer tasks to run. For
/// example, an application might use a foreground value of 30 ms for moderate
/// throttling scenarios, or match the default background value (usually 1000
/// ms). The WebView2 Runtime will try to respect the preferred interval set
david-risney marked this conversation as resolved.
Show resolved Hide resolved
/// by the application, but the effective value will be constrained by
/// resource and platform limitations. Setting a value of `0` means no
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// resource and platform limitations. Setting a value of `0` means no
/// resource and platform limitations. Setting a value of `0` means a preference of no throttling be applied.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[pending]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

/// throttling will be applied.
HRESULT SetThrottlingIntervalPreference(
[in] COREWEBVIEW2_THROTTLING_CATEGORY category,
david-risney marked this conversation as resolved.
Show resolved Hide resolved
[in] UINT32 intervalInMilliseconds);
david-risney marked this conversation as resolved.
Show resolved Hide resolved
}

/// A continuation of the `ICoreWebView2Frame` interface to support IsUntrusted property.
[uuid(5b7d1b96-699b-44a2-b9f1-b8e88f9ac2be), object, pointer_default(unique)]
interface ICoreWebView2Frame6 : ICoreWebView2Frame5 {
/// The `IsUntrusted` property indicates whether the frame has been marked
/// untrusted by the host app. Untrusted frames will receive a different
/// script throttling category as compared to regular frames. Defaults to
/// `FALSE` unless set otherwise. When `FALSE`, and for main frame, throttling
/// category will be determined by page state. The corresponding preferred
/// interval will apply (set through `SetThrottlingIntervalPreference`).
[propget] HRESULT IsUntrusted([out, retval] BOOL* value);

/// Marks the frame as untrusted, for script throttling purposes.
[propput] HRESULT IsUntrusted([in] BOOL value);
}

```

```C#
namespace Microsoft.Web.WebView2.Core
{
enum CoreWebView2ThrottlingCategory
{
Foreground = 0,
Background = 1,
Intensive = 2,
UntrustedFrame = 3,
};

runtimeclass CoreWebView2
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: class here should be CoreWebView2Settings, matching the COM API above.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix!

{
// ...

[interface_name("Microsoft.Web.WebView2.Core.ICoreWebView2_21")]
{
UInt32 GetThrottlingIntervalPreference(CoreWebView2ThrottlingCategory category);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if instead of two methods on the CoreWebView2, we should make this four get/set properties on the CoreWebView2Settings.

Reading through your document it doesn't seem like its useful to have a method that takes this as an enum because the caller will always be explicitly changing the value of a specific category. In which case its probably easier to have individual properties for each category rather than a method where you choose the category.

And then the whole scenario seems very uncommon so rather than put this on the CoreWebView2 we can move it at least to the CoreWebView2Settings.

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no strong opinion on whether to put these in CoreWebView2 or CoreWebView2Settings. Looking through our existing APIs, I see that there are properties in the settings object which are applied immediately, so I think we can use the same pattern here.

For enum vs methods. I don't have a strong preference either, but I'm not sure I understand the reasoning:

the caller will always be explicitly changing the value of a specific category

isn't this what we do for other APIs taking non-flags enum types?
Only thing is I think the enum makes it easier to find what the possible/configurable states are.

Copy link
Contributor Author

@lflores-ms lflores-ms Nov 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is what it would look like moving as properties to CoreWebView2Settings. Is this what you are suggesting?

namespace Microsoft.Web.WebView2.Core
{

    runtimeclass CoreWebView2Settings
    {
        // ...

        UInt32 ForegroundThrottlingIntervalPreference { get; set; }
        UInt32 BackgroundThrottlingIntervalPreference { get; set; }
        UInt32 IntensiveThrottlingIntervalPreference { get; set; }
        UInt32 IsolatedThrottlingIntervalPreference { get; set; }
    }

    runtimeclass CoreWebView2Frame
    {
        // ...
        
        Boolean ShouldUseIsolatedThrottling { get; set; };
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Undecided on the name pattern though. I think we'd want them to show up together so maybe the pattern should be something like ThrottlingIntervalPreference<category> instead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Giving them the same prefix does help group them although the name is less readable. Let's try that in the API review and see what they say

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just pushed the update moving to properties in settings object. Thanks!


void SetThrottlingIntervalPreference(CoreWebView2ThrottlingCategory category, UInt32 intervalInMilliseconds);
}
}

runtimeclass CoreWebView2Frame
{
// ...

[interface_name("Microsoft.Web.WebView2.Core.ICoreWebView2Frame6")]
{
Boolean IsUntrusted { get; set; };
}
}
}

```

# Appendix