Proposal: Budget API


#1

See the GitHub Budget API repository for an explainer, use cases, examples and a draft specification, as well as a document comparing behaviour in both browsers and operating systems.

To summarize: historically, the ability to run code on the user’s devices has been tied to having an open page or tab in their browser. Background Sync, the Push API, Background Fetch and others have changed this, by enabling the developer to influence when things happen, even when the user isn’t using their browser at all.

UAs have chosen different ways to protect the user. For instance, when using the Push API without notifications, Firefox may suspend a the subscription, whereas Chrome may show one of its own. Such measures are based on heuristics— for instance, the level of engagement the user has with the website makes either browser be more lenient.

The Budget API is a generalized way to tell the developer whether they have the ability to exercise such background operations without the side-effects imposed by the user agent.


#2

I think we have a big need for an API like this.

Enabling features like background sync, “silent push” notifications and other “background scheduling” APIs is IMHO blocked until we introduce some concept like this.

Given that, I think as a next step we should move the repo over to the WICG repo and keep working on it there.


#3

Given that this has been also circulated around with Mozilla, we would like to see it progress.

@beverloo, I’ll work with you on migrating it over the the WICG.


#4

I’m also interested in seeing progress. That said, I think that we can make a few refinements, but I don’t have fully-formed opinions about them. One idea is that reservations might be made implicit in the use of certain APIs.


#5

Ok, we’ve moved the spec to the WICG:

People interested in the spec, please file issues there… or feel free to continue having a higher level discussion here.

The repo includes a great explainer with examples for those interested. Please check it out!


#6

Thanks very much for moving this over, @marcosc! I’m also very interested in exposing a way for sites to inspect and reserve budgets for background tasks.

Firefox currently has a quota mechanism for silent pushes, but it’s opaque (by design), not easily generalized to APIs like background sync and fetch, and developers have reported some problems with it. We end up penalizing sites that want to do the right thing, which is frustrating and surprising.


#7

tl;dr: I don’t think that we need this API. We could (and should) implement a way for applications to learn how many “silent” pushes they have remaining.

I realize that it’s taken a long time for me to produce this analysis. That’s entirely my own fault, I had reservations about the API that I had trouble forming into something concrete and I really hate non-specific grumbling. I definitely owe Peter a drink over this one.


Purpose

The entire reason that the Budget API [1] exists is around our collective attempts to retain some degree of accountability for sites [2].

In designing the Push API [3] we discovered that we were developing a way for sites to run at arbitrary times, in the background. That removes accountability for sites. Web browsers don’t have UX for background activity. We sort of get away with this for fetch and service workers because they only run in response to foreground activity (more or less [4]). However, with Push and Background Sync [5], the application gets complete control over when this activity occurs.

The reaction to this was to limit the amount of background activity a site gets. A site would be able to activate a service worker a limited number times between site visits. Visit the site and you would get some more credits to spend on push. We eventually decided not to penalize sites that show notifications, on the principle that the notification includes

The solution that this API proposes has two parts. Firstly, let applications see the budget they are given, let them query it. Secondly, let them reserve resources. This second part is the important one, it allows applications to gain some certainty.

The Use Case is Real

The underlying problem is a genuine annoyance. Sites have a real hard time with the budget we set. It’s especially bad because messages just stop arriving and no one knows why.

It’s particularly hard for third-party push vendors, who struggle with the requirement to have users visit their origin. We don’t credit third-party loads (iframes and the like), but that is often the only interaction that users have with these senders. Other than to make it possible for these applications to learn the parameters of their confinement, the proposal doesn’t really do a lot of alleviate that problem.

Query Methods are more Liability than Asset

The definition of query methods for examining an origin’s budget is a little too optimistic for me. I would remove getBudget() and getCost() from the proposal.

The notion that the browser has a fixed hidden variable for each site that it can then predict might be close to what we have today, but I’m concerned that it will cause us to commit to a particular model of interaction that could constrain us in ways we don’t understand.

For instance, this assumes a single budget, which might be the right simplification, but it could prevent a browser from tracking different APIs separately. The browser might be able to synthesize a single value, but that might lose information.

The way that future values are obtained with getBudget() is unrealistic, I think. If anything like this were to exist, I would prefer a query semantic “if I asked for X at time Y, do you think that you would allow it?” That leads to my suggestion to extend permissions (below).

Too Generic

Names matter. This API uses the generic “Budget” term without consideration for the narrower scope of its applicability. It’s very clear in the proposal that this is for background tasks only, but the name doesn’t capture that.

The presumption here is that it would be useful for things other than what it is being used for today, but that’s overly optimistic in my view. I’ve too much experience with engineering optimistic generality into systems only to find that the extra effort was wasted. YAGNI applies.

For background sync, the API doesn’t really apply right now, and it might never. Background sync only really runs as a consequence of activity, and while push might be the sort of activity that triggers the need for a sync (see [6]), it doesn’t seem like background sync can be run on a timer any more. The lastChance attribute of SyncEvent more or less covers the use case that .reserve() does. (I’ve more on this point below.)

That suggests that background sync could be a non-customer of this feature, leaving only push, where a more direct approach might work. If a background sync were to be created that used a timer, that could build a reservation system in directly. For example, if background sync were to acquire a second argument to .register() that indicated how long to wait before attempting a sync, that method might throw if the site budget was overcommitted.

.request() Alternative: Extend Permissions

One different way of looking at this problem is to consider it part of the permissions API. Right now, that API assumes a fairly simplistic model where different capabilities are allowed, denied, or as yet unknown (usually because they involve asking users for permission). A possible fourth state might be added: use-limited (name negotiable). The API could be used to reserve uses of different capabilities. For instance,

const desc = { name: 'push', userVisibleOnly: false };
const status = await permissions.query(desc);
if (status === 'use-limited') {
 const reserved = await permissions.reserve(desc);
 if (reserved) {
   // do something that might enable push
 }
}

However, that’s a superficial change and not really getting into the real issues. Though integration might address issue #14 from the TAG review.

Reserve the World

Based on the current API, I can see the following code being written:

let reservations = loadExistingCountFromIndexedDb() || 0;
while (await navigator.budget.reserve('silent-push')) {
  ++reservations;
}
console.log(`We can send {reservations} more silent push messages.`);

That would be sad, because it somewhat misses the point of the API. But it’s a very useful reduction in complexity for sites. If PushManager exposed a silentPushesRemaining count (the name is a strawman) that would save the looping.

The information that is lost by way of this simplification is that allowances might decay over time, even with the site being active. That makes silentPushesRemaining a false promise, because though it might say 10 today, after 72 hours it might be reduced to 4 even if a silent push wasn’t sent. I think that it is best to address that with documentation, noting that this number speaks of independent activations over the next day (or some fixed time period).

This could be reduced further to a youHaveToShowANotification boolean on PushEvent. That was suggested at one point, but that removes critical information. The problem with a lastChance-style boolean is that it doesn’t really give an application enough information to make longer-term decisions. It can’t plan out its strategy for pushing messages when it doesn’t know how many it has to spend.

Just a Simple Push API Attribute

The main weakness with .request() in my opinion is that it isn’t necessary. We built the push API without any notion of budget and so an application can use the API without having to concern themselves with such things. Without any API, the user agent that limits push usage finds that sites break in surprising ways.

I spent a lot of time trying to think about a way to make .reserve() or something like it critical to the functioning of the API, but the inescapable reality here is that sites are using push today. Had we built the API with the notion at its core, well, we’d not be stuck grappling with this issue.

A silentPushesRemaining attribute would seem to cover most of the use cases with greater certainty. The problem of applications that were built before this feature is added remains, but those applications are currently either stuck without silent pushes, or they have to deal with the apparent capriciousness of a browser that suddenly stops delivering messages.

As such, we only have to deal with applications that have been updated to use this API. Those applications can easily check whether a silent push was permitted by looking at a silentPushesRemaining attribute (or an appropriately asynchronous getter).

(I’m aware that the Budget API allows for the actual cost of an operation to be lower than the expected cost. That’s easy to account for by recalculating the silentPushesRemaining value over time; if actual costs were lower than expected, you can easily increase the allowance to reflect that.)

This view is further supported by the observation that .reserve() is effectively implicit right now: every time you have an active push subscription, you always have at least one reservation. Having .reserve() return false is no different to silentPushesRemaining becoming 0 or a lastChance flag.

Conclusion

BudgetService.reserve() is solving a real problem. However .reserve() could be replaced with a much simpler feature: an attribute on the Push API.

Other aspects of the API are probably creating more problems than they solve and should be removed.

[1] https://wicg.github.io/budget-api/ [2] https://docs.google.com/presentation/d/1soMBhvsd0wuk8PO5UjtrMd116Z95jJiZLxlUFomyBFs [3] https://w3c.github.io/push-api/ [4] I still don’t understand what happens if we want to show a slow script warning for a service worker… [5] https://wicg.github.io/BackgroundSync/spec/ [6] https://github.com/w3c/push-api/issues/240 - this never got raised on background sync, from what I can see


#8

I half agree with Martin which is more than I’ve ever said before. Asking the user to function as a class-scheduler is beyond the pale.

This is a permissions problem. Simple.

Why was “Budget” chosen? “Quota” too 80s?