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

Expand use cases document #53

Merged
merged 3 commits into from
Sep 5, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
39 changes: 10 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ function implicit() {
program();
```

Furthermore, the async/await syntax bypasses the userland Promises and
makes it impossible for existing tools like [Zone.js](#zonesjs) that
[instruments](https://github.com/angular/angular/blob/main/packages/zone.js/STANDARD-APIS.md)
the `Promise` to work with it without transpilation.

This proposal introduces a general mechanism by which lost implicit call site
information can be captured and used across transitions through the event loop,
while allowing the developer to write async code largely as they do in cases
Expand All @@ -143,14 +148,15 @@ required for special handling async code in such cases.
This proposal introduces APIs to propagate a value through asynchronous code,
such as a promise continuation or async callbacks.

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

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

# Proposed Solution

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

```typescript
Expand Down Expand Up @@ -266,37 +272,12 @@ runWhenIdle(() => {
> Note: There are controversial thought on the dynamic scoping and
> `Variable`, checkout [SCOPING.md][] for more details.

## Use cases

Use cases for async context include:

- Annotating logs with information related to an asynchronous callstack.

- Collecting performance information across logical asynchronous threads of
control.

- Web APIs such as
[Prioritized Task Scheduling](https://wicg.github.io/scheduling-apis).

- There are a number of use cases for browsers to track the attribution of tasks
in the event loop, even though an asynchronous callstack. They include:

- Optimizing the loading of critical resources in web pages requires tracking
whether a task is transitively depended on by a critical resource.

- Tracking long tasks effectively with the
[Long Tasks API](https://w3c.github.io/longtasks) requires being able to
tell where a task was spawned from.

- [Measuring the performance of SPA soft navigations](https://developer.chrome.com/blog/soft-navigations-experiment/)
requires being able to tell which task initiated a particular soft
navigation.

Hosts are expected to use the infrastructure in this proposal to allow tracking
not only asynchronous callstacks, but other ways to schedule jobs on the event
loop (such as `setTimeout`) to maximize the value of these use cases.

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

# Examples

Expand Down
204 changes: 202 additions & 2 deletions USE-CASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ Use cases for `AsyncContext` include:
control. This includes timing measurements, as well as OpenTelemetry. For
example, OpenTelemetry's
[`ZoneContextManager`](https://open-telemetry.github.io/opentelemetry-js/classes/_opentelemetry_context_zone_peer_dep.ZoneContextManager.html)
is only able to achieve this by using zone.js (see the prior art section
below).
is only able to achieve this by using zone.js (see the [prior arts section](./README.md#prior-arts)).

- Web APIs such as
[Prioritized Task Scheduling](https://wicg.github.io/scheduling-apis) let
Expand Down Expand Up @@ -138,3 +137,204 @@ export function run<T>(id: string, cb: () => T) {
context.run(id, cb);
}
```

## Use Case: Soft Navigation Heuristics

When a user interacts with the page, it's critical that the app feels fast.
But there's no way to determine what started this chain of interaction when
the final result is ready to patch into the DOM tree. The problem becomes
more prominent if the interaction involves with several asynchronous
operations since their original call stack has gone.

```typescript
// Framework listener
doc.addEventListener('click', () => {
context.run(Date.now(), async () => {
// User code
const f = await fetch(dataUrl);
patch(doc, await f.json());
});
});
// Some framework code
const context = new AsyncContext();
function patch(doc, data) {
doLotsOfWork(doc, data, update);
}
function update(doc, html) {
doc.innerHTML = html;
// Calculate the duration of the user interaction from the value in the
// AsyncContext instance.
const duration = Date.now() - context.get();
}
```

## Use Case: Transitive Task Attributes

Browsers can schedule tasks with priorities attributes. However, the task
priority attribution is not transitive at the moment.

```typescript
async function task() {
startWork();
await scheduler.yield();
doMoreWork();
// Task attributes are lost after awaiting.
let response = await fetch(myUrl);
let data = await response.json();
process(data);
}

scheduler.postTask(task, {priority: 'background'});
```

The task may include the following attributes:
- Execution priority,
- Fetch priority,
- Privacy protection attributes.

With the mechanism of `AsyncContext` in the language, tasks attributes can be
transitively propagated.

```typescript
const res = await scheduler.postTask(task, {
priority: 'background',
});
console.log(res);

async function task() {
// Fetch remains background priority.
const resp = await fetch('/hello');
const text = await resp.text();

// doStuffs should schedule background tasks by default.
return doStuffs(text);
}

async function doStuffs(text) {
// Some async calculation...
return text;
}
```

## Use Case: Userspace telemetry

Application performance monitoring libraries like [OpenTelemetry][] can save
their tracing spans in an `AsyncContext` and retrieves the span when they determine
what started this chain of interaction.

It is a requirement that these libraries can not intrude the developer APIs
for seamless monitoring.

```typescript
doc.addEventListener('click', () => {
// Create a span and records the performance attributes.
const span = tracer.startSpan('click');
context.run(span, async () => {
const f = await fetch(dataUrl);
patch(dom, await f.json());
});
});

const context = new AsyncContext();
function patch(dom, data) {
doLotsOfWork(dom, data, update);
}
function update(dom, html) {
dom.innerHTML = html;
// Mark the chain of interaction as ended with the span
const span = context.get();
span?.end();
}
```

### User Interaction

OpenTelemetry instruments user interaction with document elements and connects
subsequent network requests and history state changes with the user
interaction.

The propagation of spans can be achieved with `AsyncContext` and helps
distinguishing the initiators (document load, or user interaction).

```typescript
registerInstrumentations({
instrumentations: [new UserInteractionInstrumentation()],
});

// Subsequent network requests are associated with the user-interaction.
const btn = document.getElementById('my-btn');
btn.addEventListener('click', () => {
fetch('https://httpbin.org/get')
.then(() => {
console.log('data downloaded 1');
return fetch('https://httpbin.org/get');
});
.then(() => {
console.log('data downloaded 2');
});
});
```

Read more at [opentelemetry/user-interaction](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/web/opentelemetry-instrumentation-user-interaction).

### Long task initiator

Tracking long tasks effectively with the [Long Tasks API](https://github.com/w3c/longtasks)
requires being able to tell where a task was spawned from.

However, OpenTelemetry is not able to associate the Long Task timing entry
with their initiating trace spans. Capturing the `AsyncContext` can help here.

Notably, this proposal doesn't solve the problem solely. It provides a path
forward to the problem and can be integrated into the Long Tasks API.

```typescript
registerInstrumentations({
instrumentations: [new LongTaskInstrumentation()],
});
// Roughly equals to
new PerformanceObserver(list => {...})
.observe({ entryTypes: ['longtask'] });

// Perform a 50ms long task
function myTask() {
const start = Date.now();
while (Date.now() - start <= 50) {}
}

myTask();
```

Read more at [opentelemetry/long-task](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/web/opentelemetry-instrumentation-long-task).

### Resource Timing Attributes

OpenTelemetry instruments fetch API with network timings from [Resource Timing API](https://github.com/w3c/resource-timing/)
associated to the initiator fetch span.

Without resource timing initiator info, it is not an intuitive approach to
associate the resource timing with the initiator spans. Capturing the
`AsyncContext` can help here.

Notably, this proposal doesn't solve the problem solely. It provides a path
forward to the problem and can be integrated into the Long Tasks API.

```typescript
registerInstrumentations({
instrumentations: [new FetchInstrumentation()],
});
// Observes network events and associate them with spans.
new PerformanceObserver(list => {
const entries = list.getEntries();
spans.forEach(span => {
const entry = entries.find(it => {
return it.name === span.name && it.startTime >= span.startTime;
});
span.recordNetworkEvent(entry);
});
}).observe({ entryTypes: ['resource'] });
```

Read more at [opentelemetry/fetch](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-fetch).

[OpenTelemetry]: https://github.com/open-telemetry/opentelemetry-js