Skip to content

Commit 2fdde51

Browse files
committed
Add backpressure.{inc,dec}, deprecate backpressure.set
1 parent 2c17516 commit 2fdde51

File tree

5 files changed

+103
-28
lines changed

5 files changed

+103
-28
lines changed

design/mvp/Async.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -466,9 +466,13 @@ export calls can start piling up, each consuming some of the component's finite
466466
private resources (like linear memory), requiring the component to be able to
467467
exert *backpressure* to allow some tasks to finish (and release private
468468
resources) before admitting new async export calls. To do this, a component may
469-
call the [`backpressure.set`] built-in to set a component-instance-wide
470-
"backpressure" flag that causes subsequent export calls to immediately return
471-
in the "starting" state without calling the component's Core WebAssembly code.
469+
call the [`backpressure.inc`] built-in to increment an instance-wide
470+
"backpressure" counter until resources are freed and then call
471+
[`backpressure.dec`] to decrement the counter. When the backpressure counter is
472+
greater than zero, new export calls immediately return in the "starting" state
473+
without calling the component's Core WebAssembly code. By using a counter
474+
instead of a boolean flag, unrelated pieces of code can report backpressure for
475+
their own local resources without prior coordination.
472476

473477
In addition to *explicit* backpressure set by wasm code, there is also an
474478
*implicit* source of backpressure used to protect non-reentrant core wasm code.
@@ -1137,7 +1141,8 @@ comes after:
11371141
[Canonical Built-in]: Explainer.md#canonical-built-ins
11381142
[`context.get`]: Explainer.md#-contextget
11391143
[`context.set`]: Explainer.md#-contextset
1140-
[`backpressure.set`]: Explainer.md#-backpressureset
1144+
[`backpressure.inc`]: Explainer.md#-backpressureinc-and-backpressuredec
1145+
[`backpressure.dec`]: Explainer.md#-backpressureinc-and-backpressuredec
11411146
[`task.return`]: Explainer.md#-taskreturn
11421147
[`task.cancel`]: Explainer.md#-taskcancel
11431148
[`subtask.cancel`]: Explainer.md#-subtaskcancel
@@ -1155,7 +1160,6 @@ comes after:
11551160
[`unpack_callback_result`]: CanonicalABI.md#canon-lift
11561161
[`canon_lower`]: CanonicalABI.md#canon-lower
11571162
[`canon_context_get`]: CanonicalABI.md#-canon-contextget
1158-
[`canon_backpressure_set`]: CanonicalABI.md#-canon-backpressureset
11591163
[`canon_waitable_set_wait`]: CanonicalABI.md#-canon-waitable-setwait
11601164
[`canon_task_return`]: CanonicalABI.md#-canon-taskreturn
11611165
[`canon_task_cancel`]: CanonicalABI.md#-canon-taskcancel

design/mvp/CanonicalABI.md

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ being specified here.
4343
* [`canon resource.rep`](#canon-resourcerep)
4444
* [`canon context.get`](#-canon-contextget) 🔀
4545
* [`canon context.set`](#-canon-contextset) 🔀
46-
* [`canon backpressure.set`](#-canon-backpressureset) 🔀
46+
* [`canon backpressure.set`](#-canon-backpressureset) 🔀✕
47+
* [`canon backpressure.{inc,dec}`](#-canon-backpressureincdec) 🔀
4748
* [`canon task.return`](#-canon-taskreturn) 🔀
4849
* [`canon task.cancel`](#-canon-taskcancel) 🔀
4950
* [`canon yield`](#-canon-yield) 🔀
@@ -279,15 +280,15 @@ class ComponentInstance:
279280
store: Store
280281
table: Table
281282
may_leave: bool
282-
backpressure: bool
283+
backpressure: int
283284
exclusive: bool
284285
num_waiting_to_enter: int
285286

286287
def __init__(self, store):
287288
self.store = store
288289
self.table = Table()
289290
self.may_leave = True
290-
self.backpressure = False
291+
self.backpressure = 0
291292
self.exclusive = False
292293
self.num_waiting_to_enter = 0
293294
```
@@ -829,8 +830,8 @@ to avoid the need to otherwise-endlessly allocate guest memory for blocked
829830
async calls until OOM. When backpressure is enabled, `enter` will block until
830831
backpressure is disabled. There are three sources of backpressure:
831832
1. *Explicit backpressure* is triggered by core wasm calling
832-
`backpressure.set` which, in `canon_backpressure_set` (defined below),
833-
sets the `ComponentInstance.backpressure` flag.
833+
`backpressure.{inc,dec}` which, in `canon_backpressure_{inc,dec}`
834+
(defined below) modify the `ComponentInstance.backpressure` counter.
834835
2. *Implicit backpressure* triggered when `Task.needs_exclusive()` is true and
835836
the `exclusive` lock is already held.
836837
3. *Residual backpressure* triggered by explicit or implicit backpressure
@@ -842,7 +843,7 @@ backpressure is disabled. There are three sources of backpressure:
842843
def enter(self):
843844
assert(self.thread is not None)
844845
def has_backpressure():
845-
return self.inst.backpressure or (self.needs_exclusive() and self.inst.exclusive)
846+
return self.inst.backpressure > 0 or (self.needs_exclusive() and self.inst.exclusive)
846847
if has_backpressure() or self.inst.num_waiting_to_enter > 0:
847848
self.inst.num_waiting_to_enter += 1
848849
completed = self.thread.suspend_until(lambda: not has_backpressure(), cancellable = True)
@@ -3523,7 +3524,12 @@ def canon_context_set(t, i, task, v):
35233524
```
35243525

35253526

3526-
### 🔀 `canon backpressure.set`
3527+
### 🔀✕ `canon backpressure.set`
3528+
3529+
> This built-in is deprecated in favor of `backpressure.{inc,dec}` and will be
3530+
> removed once producer tools have transitioned. Producer tools should avoid
3531+
> emitting calls to both `set` and `inc`/`dec` since `set` will clobber the
3532+
> counter.
35273533
35283534
For a canonical definition:
35293535
```wat
@@ -3532,16 +3538,42 @@ For a canonical definition:
35323538
validation specifies:
35333539
* `$f` is given type `(func (param $enabled i32))`
35343540

3535-
Calling `$f` invokes the following function, which sets or clears the
3536-
`ComponentInstance.backpressure` flag. `Task.enter` waits for this flag to be
3537-
clear before allowing new tasks to start.
3541+
Calling `$f` invokes the following function, which sets the `backpressure`
3542+
counter to `1` or `0`. `Task.enter` waits for `backpressure` to be `0` before
3543+
allowing new tasks to start.
35383544
```python
35393545
def canon_backpressure_set(task, flat_args):
3540-
trap_if(task.opts.sync)
35413546
assert(len(flat_args) == 1)
3542-
task.inst.backpressure = bool(flat_args[0])
3547+
task.inst.backpressure = int(bool(flat_args[0]))
3548+
return []
3549+
```
3550+
3551+
### 🔀 `canon backpressure.{inc,dec}`
3552+
3553+
For a canonical definition:
3554+
```wat
3555+
(canon backpressure.inc (core func $inc))
3556+
(canon backpressure.dec (core func $dec))
3557+
```
3558+
validation specifies:
3559+
* `$inc`/`$dec` are given type `(func)`
3560+
3561+
Calling `$inc` or `$dec` invokes one of the following functions:
3562+
```python
3563+
def canon_backpressure_inc(task):
3564+
assert(0 <= task.inst.backpressure < 2**16)
3565+
task.inst.backpressure += 1
3566+
trap_if(task.inst.backpressure == 2**16)
3567+
return []
3568+
3569+
def canon_backpressure_dec(task):
3570+
assert(0 <= task.inst.backpressure < 2**16)
3571+
task.inst.backpressure -= 1
3572+
trap_if(task.inst.backpressure < 0)
35433573
return []
35443574
```
3575+
`Task.enter` waits for `backpressure` to return to `0` before allowing new
3576+
tasks to start, implementing [backpressure].
35453577

35463578

35473579
### 🔀 `canon task.return`

design/mvp/Explainer.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1417,7 +1417,9 @@ canon ::= ...
14171417
| (canon resource.rep <typeidx> (core func <id>?))
14181418
| (canon context.get <valtype> <u32> (core func <id>?)) 🔀
14191419
| (canon context.set <valtype> <u32> (core func <id>?)) 🔀
1420-
| (canon backpressure.set (core func <id>?)) 🔀
1420+
| (canon backpressure.set (core func <id>?)) 🔀✕
1421+
| (canon backpressure.inc (core func <id>?)) 🔀
1422+
| (canon backpressure.dec (core func <id>?)) 🔀
14211423
| (canon task.return (result <valtype>)? <canonopt>* (core func <id>?)) 🔀
14221424
| (canon task.cancel (core func <id>?)) 🔀
14231425
| (canon yield cancellable? (core func <id>?)) 🔀
@@ -1580,7 +1582,10 @@ future (as described [here][context-local storage]).
15801582

15811583
For details, see [`canon_context_set`] in the Canonical ABI explainer.
15821584

1583-
###### 🔀 `backpressure.set`
1585+
###### 🔀✕ `backpressure.set`
1586+
1587+
> This built-in is deprecated in favor of `backpressure.{inc,dec}` and will be
1588+
> removed once producer tools have transitioned.
15841589
15851590
| Synopsis | |
15861591
| -------------------------- | --------------------- |
@@ -1594,6 +1599,26 @@ to the component (until the flag is unset). This allows the component to exert
15941599

15951600
For details, see [`canon_backpressure_set`] in the Canonical ABI explainer.
15961601

1602+
###### 🔀 `backpressure.inc` and `backpressure.dec`
1603+
1604+
| Synopsis | |
1605+
| -------------------------- | ---------- |
1606+
| Approximate WIT signature | `func()` |
1607+
| Canonical ABI signature | `[] -> []` |
1608+
1609+
The `backpressure.{inc,dec}` built-ins allow code running in a component to
1610+
prevent new incoming export calls to the component by enabling [backpressure].
1611+
These built-ins increment and decrement a per-component-instance counter that,
1612+
when greater than zero, enables backpressure.
1613+
1614+
If these built-ins would overflow or underflow a 16-bit unsigned integer, they
1615+
trap instead. As an composable convention, each piece of code that calls
1616+
`backpressure.inc` must take responsibility for calling `backpressure.dec`
1617+
exactly once when the source of backpressure subsides.
1618+
1619+
For details, see [`canon_backpressure_{inc,dec}`] in the Canonical ABI
1620+
explainer.
1621+
15971622
###### 🔀 `task.return`
15981623

15991624
The `task.return` built-in takes as parameters the result values of the
@@ -3043,6 +3068,7 @@ For some use-case-focused, worked examples, see:
30433068
[`canon_context_get`]: CanonicalABI.md#-canon-contextget
30443069
[`canon_context_set`]: CanonicalABI.md#-canon-contextset
30453070
[`canon_backpressure_set`]: CanonicalABI.md#-canon-backpressureset
3071+
[`canon_backpressure_{inc,dec}`]: CanonicalABI.md#-canon-backpressureincdec
30463072
[`canon_task_return`]: CanonicalABI.md#-canon-taskreturn
30473073
[`canon_task_cancel`]: CanonicalABI.md#-canon-taskcancel
30483074
[`canon_yield`]: CanonicalABI.md#-canon-yield

design/mvp/canonical-abi/definitions.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -251,15 +251,15 @@ class ComponentInstance:
251251
store: Store
252252
table: Table
253253
may_leave: bool
254-
backpressure: bool
254+
backpressure: int
255255
exclusive: bool
256256
num_waiting_to_enter: int
257257

258258
def __init__(self, store):
259259
self.store = store
260260
self.table = Table()
261261
self.may_leave = True
262-
self.backpressure = False
262+
self.backpressure = 0
263263
self.exclusive = False
264264
self.num_waiting_to_enter = 0
265265

@@ -540,7 +540,7 @@ def needs_exclusive(self):
540540
def enter(self):
541541
assert(self.thread is not None)
542542
def has_backpressure():
543-
return self.inst.backpressure or (self.needs_exclusive() and self.inst.exclusive)
543+
return self.inst.backpressure > 0 or (self.needs_exclusive() and self.inst.exclusive)
544544
if has_backpressure() or self.inst.num_waiting_to_enter > 0:
545545
self.inst.num_waiting_to_enter += 1
546546
completed = self.thread.suspend_until(lambda: not has_backpressure(), cancellable = True)
@@ -2109,9 +2109,22 @@ def canon_context_set(t, i, task, v):
21092109
### 🔀 `canon backpressure.set`
21102110

21112111
def canon_backpressure_set(task, flat_args):
2112-
trap_if(task.opts.sync)
21132112
assert(len(flat_args) == 1)
2114-
task.inst.backpressure = bool(flat_args[0])
2113+
task.inst.backpressure = int(bool(flat_args[0]))
2114+
return []
2115+
2116+
### 🔀 `canon backpressure.{inc,dec}`
2117+
2118+
def canon_backpressure_inc(task):
2119+
assert(0 <= task.inst.backpressure < 2**16)
2120+
task.inst.backpressure += 1
2121+
trap_if(task.inst.backpressure == 2**16)
2122+
return []
2123+
2124+
def canon_backpressure_dec(task):
2125+
assert(0 <= task.inst.backpressure < 2**16)
2126+
task.inst.backpressure -= 1
2127+
trap_if(task.inst.backpressure < 0)
21152128
return []
21162129

21172130
### 🔀 `canon task.return`

design/mvp/canonical-abi/run_tests.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -565,11 +565,11 @@ def core_eager_producer(task, args):
565565
fut1_2 = RacyBool(False)
566566
def core_toggle(task, args):
567567
assert(len(args) == 0)
568-
[] = canon_backpressure_set(task, [1])
568+
[] = canon_backpressure_inc(task)
569569
task.thread.suspend_until(fut1_1.is_set)
570570
[] = canon_task_return(task, [], producer_opts, [])
571571
task.thread.suspend_until(fut1_2.is_set)
572-
[] = canon_backpressure_set(task, [0])
572+
[] = canon_backpressure_dec(task)
573573
return []
574574
toggle_callee = partial(canon_lift, producer_opts, producer_inst, toggle_ft, core_toggle)
575575

@@ -1038,9 +1038,9 @@ def test_async_backpressure():
10381038
producer1_done = False
10391039
def producer1_core(task, args):
10401040
nonlocal producer1_done
1041-
canon_backpressure_set(task, [1])
1041+
canon_backpressure_inc(task)
10421042
task.thread.suspend_until(fut.is_set)
1043-
canon_backpressure_set(task, [0])
1043+
canon_backpressure_dec(task)
10441044
canon_task_return(task, [], producer_opts, [])
10451045
producer1_done = True
10461046
return []

0 commit comments

Comments
 (0)