Skip to content

Commit bab2505

Browse files
Expand use cases document (tc39#53)
* Expand use cases document * fixup! --------- Co-authored-by: Justin Ridgewell <[email protected]>
1 parent 27e5abe commit bab2505

File tree

2 files changed

+212
-31
lines changed

2 files changed

+212
-31
lines changed

README.md

+10-29
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,11 @@ function implicit() {
132132
program();
133133
```
134134

135+
Furthermore, the async/await syntax bypasses the userland Promises and
136+
makes it impossible for existing tools like [Zone.js](#zonesjs) that
137+
[instruments](https://github.com/angular/angular/blob/main/packages/zone.js/STANDARD-APIS.md)
138+
the `Promise` to work with it without transpilation.
139+
135140
This proposal introduces a general mechanism by which lost implicit call site
136141
information can be captured and used across transitions through the event loop,
137142
while allowing the developer to write async code largely as they do in cases
@@ -143,14 +148,15 @@ required for special handling async code in such cases.
143148
This proposal introduces APIs to propagate a value through asynchronous code,
144149
such as a promise continuation or async callbacks.
145150

146-
Non-goals:
151+
Compared to the [Prior Arts](#prior-arts), this proposal identifies the
152+
following features as non-goals:
147153

148154
1. Async tasks scheduling and interception.
149155
1. Error handling & bubbling through async stacks.
150156

151157
# Proposed Solution
152158

153-
`AsyncContext` are designed as a value store for context propagation across
159+
`AsyncContext` is designed as a value store for context propagation across
154160
logically-connected sync/async code execution.
155161

156162
```typescript
@@ -266,37 +272,12 @@ runWhenIdle(() => {
266272
> Note: There are controversial thought on the dynamic scoping and
267273
> `Variable`, checkout [SCOPING.md][] for more details.
268274
269-
## Use cases
270-
271-
Use cases for async context include:
272-
273-
- Annotating logs with information related to an asynchronous callstack.
274-
275-
- Collecting performance information across logical asynchronous threads of
276-
control.
277-
278-
- Web APIs such as
279-
[Prioritized Task Scheduling](https://wicg.github.io/scheduling-apis).
280-
281-
- There are a number of use cases for browsers to track the attribution of tasks
282-
in the event loop, even though an asynchronous callstack. They include:
283-
284-
- Optimizing the loading of critical resources in web pages requires tracking
285-
whether a task is transitively depended on by a critical resource.
286-
287-
- Tracking long tasks effectively with the
288-
[Long Tasks API](https://w3c.github.io/longtasks) requires being able to
289-
tell where a task was spawned from.
290-
291-
- [Measuring the performance of SPA soft navigations](https://developer.chrome.com/blog/soft-navigations-experiment/)
292-
requires being able to tell which task initiated a particular soft
293-
navigation.
294-
295275
Hosts are expected to use the infrastructure in this proposal to allow tracking
296276
not only asynchronous callstacks, but other ways to schedule jobs on the event
297277
loop (such as `setTimeout`) to maximize the value of these use cases.
298278

299-
A detailed example usecase can be found [here](./USE-CASES.md)
279+
A detailed example of use cases can be found in the
280+
[Use cases document](./USE-CASES.md).
300281

301282
# Examples
302283

USE-CASES.md

+202-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ Use cases for `AsyncContext` include:
66
control. This includes timing measurements, as well as OpenTelemetry. For
77
example, OpenTelemetry's
88
[`ZoneContextManager`](https://open-telemetry.github.io/opentelemetry-js/classes/_opentelemetry_context_zone_peer_dep.ZoneContextManager.html)
9-
is only able to achieve this by using zone.js (see the prior art section
10-
below).
9+
is only able to achieve this by using zone.js (see the [prior arts section](./README.md#prior-arts)).
1110

1211
- Web APIs such as
1312
[Prioritized Task Scheduling](https://wicg.github.io/scheduling-apis) let
@@ -138,3 +137,204 @@ export function run<T>(id: string, cb: () => T) {
138137
context.run(id, cb);
139138
}
140139
```
140+
141+
## Use Case: Soft Navigation Heuristics
142+
143+
When a user interacts with the page, it's critical that the app feels fast.
144+
But there's no way to determine what started this chain of interaction when
145+
the final result is ready to patch into the DOM tree. The problem becomes
146+
more prominent if the interaction involves with several asynchronous
147+
operations since their original call stack has gone.
148+
149+
```typescript
150+
// Framework listener
151+
doc.addEventListener('click', () => {
152+
context.run(Date.now(), async () => {
153+
// User code
154+
const f = await fetch(dataUrl);
155+
patch(doc, await f.json());
156+
});
157+
});
158+
// Some framework code
159+
const context = new AsyncContext();
160+
function patch(doc, data) {
161+
doLotsOfWork(doc, data, update);
162+
}
163+
function update(doc, html) {
164+
doc.innerHTML = html;
165+
// Calculate the duration of the user interaction from the value in the
166+
// AsyncContext instance.
167+
const duration = Date.now() - context.get();
168+
}
169+
```
170+
171+
## Use Case: Transitive Task Attributes
172+
173+
Browsers can schedule tasks with priorities attributes. However, the task
174+
priority attribution is not transitive at the moment.
175+
176+
```typescript
177+
async function task() {
178+
startWork();
179+
await scheduler.yield();
180+
doMoreWork();
181+
// Task attributes are lost after awaiting.
182+
let response = await fetch(myUrl);
183+
let data = await response.json();
184+
process(data);
185+
}
186+
187+
scheduler.postTask(task, {priority: 'background'});
188+
```
189+
190+
The task may include the following attributes:
191+
- Execution priority,
192+
- Fetch priority,
193+
- Privacy protection attributes.
194+
195+
With the mechanism of `AsyncContext` in the language, tasks attributes can be
196+
transitively propagated.
197+
198+
```typescript
199+
const res = await scheduler.postTask(task, {
200+
priority: 'background',
201+
});
202+
console.log(res);
203+
204+
async function task() {
205+
// Fetch remains background priority.
206+
const resp = await fetch('/hello');
207+
const text = await resp.text();
208+
209+
// doStuffs should schedule background tasks by default.
210+
return doStuffs(text);
211+
}
212+
213+
async function doStuffs(text) {
214+
// Some async calculation...
215+
return text;
216+
}
217+
```
218+
219+
## Use Case: Userspace telemetry
220+
221+
Application performance monitoring libraries like [OpenTelemetry][] can save
222+
their tracing spans in an `AsyncContext` and retrieves the span when they determine
223+
what started this chain of interaction.
224+
225+
It is a requirement that these libraries can not intrude the developer APIs
226+
for seamless monitoring.
227+
228+
```typescript
229+
doc.addEventListener('click', () => {
230+
// Create a span and records the performance attributes.
231+
const span = tracer.startSpan('click');
232+
context.run(span, async () => {
233+
const f = await fetch(dataUrl);
234+
patch(dom, await f.json());
235+
});
236+
});
237+
238+
const context = new AsyncContext();
239+
function patch(dom, data) {
240+
doLotsOfWork(dom, data, update);
241+
}
242+
function update(dom, html) {
243+
dom.innerHTML = html;
244+
// Mark the chain of interaction as ended with the span
245+
const span = context.get();
246+
span?.end();
247+
}
248+
```
249+
250+
### User Interaction
251+
252+
OpenTelemetry instruments user interaction with document elements and connects
253+
subsequent network requests and history state changes with the user
254+
interaction.
255+
256+
The propagation of spans can be achieved with `AsyncContext` and helps
257+
distinguishing the initiators (document load, or user interaction).
258+
259+
```typescript
260+
registerInstrumentations({
261+
instrumentations: [new UserInteractionInstrumentation()],
262+
});
263+
264+
// Subsequent network requests are associated with the user-interaction.
265+
const btn = document.getElementById('my-btn');
266+
btn.addEventListener('click', () => {
267+
fetch('https://httpbin.org/get')
268+
.then(() => {
269+
console.log('data downloaded 1');
270+
return fetch('https://httpbin.org/get');
271+
});
272+
.then(() => {
273+
console.log('data downloaded 2');
274+
});
275+
});
276+
```
277+
278+
Read more at [opentelemetry/user-interaction](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/web/opentelemetry-instrumentation-user-interaction).
279+
280+
### Long task initiator
281+
282+
Tracking long tasks effectively with the [Long Tasks API](https://github.com/w3c/longtasks)
283+
requires being able to tell where a task was spawned from.
284+
285+
However, OpenTelemetry is not able to associate the Long Task timing entry
286+
with their initiating trace spans. Capturing the `AsyncContext` can help here.
287+
288+
Notably, this proposal doesn't solve the problem solely. It provides a path
289+
forward to the problem and can be integrated into the Long Tasks API.
290+
291+
```typescript
292+
registerInstrumentations({
293+
instrumentations: [new LongTaskInstrumentation()],
294+
});
295+
// Roughly equals to
296+
new PerformanceObserver(list => {...})
297+
.observe({ entryTypes: ['longtask'] });
298+
299+
// Perform a 50ms long task
300+
function myTask() {
301+
const start = Date.now();
302+
while (Date.now() - start <= 50) {}
303+
}
304+
305+
myTask();
306+
```
307+
308+
Read more at [opentelemetry/long-task](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/web/opentelemetry-instrumentation-long-task).
309+
310+
### Resource Timing Attributes
311+
312+
OpenTelemetry instruments fetch API with network timings from [Resource Timing API](https://github.com/w3c/resource-timing/)
313+
associated to the initiator fetch span.
314+
315+
Without resource timing initiator info, it is not an intuitive approach to
316+
associate the resource timing with the initiator spans. Capturing the
317+
`AsyncContext` can help here.
318+
319+
Notably, this proposal doesn't solve the problem solely. It provides a path
320+
forward to the problem and can be integrated into the Long Tasks API.
321+
322+
```typescript
323+
registerInstrumentations({
324+
instrumentations: [new FetchInstrumentation()],
325+
});
326+
// Observes network events and associate them with spans.
327+
new PerformanceObserver(list => {
328+
const entries = list.getEntries();
329+
spans.forEach(span => {
330+
const entry = entries.find(it => {
331+
return it.name === span.name && it.startTime >= span.startTime;
332+
});
333+
span.recordNetworkEvent(entry);
334+
});
335+
}).observe({ entryTypes: ['resource'] });
336+
```
337+
338+
Read more at [opentelemetry/fetch](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-fetch).
339+
340+
[OpenTelemetry]: https://github.com/open-telemetry/opentelemetry-js

0 commit comments

Comments
 (0)