From 82295166929404cbbf81f225f582da3be538534e Mon Sep 17 00:00:00 2001 From: Chengzhong Wu Date: Thu, 4 May 2023 16:32:27 +0800 Subject: [PATCH 1/2] Expand use cases document --- README.md | 39 +++------- USE-CASES.md | 204 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 212 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index b94c293..7ac7641 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -254,37 +260,12 @@ runWhenIdle(() => { > Note: There are controversial thought on the dynamic scoping and > `AsyncContext`, checkout [SCOPING.md][] for more details. -## Use cases - -Use cases for `AsyncContext` 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 diff --git a/USE-CASES.md b/USE-CASES.md index 911760a..7d5a896 100644 --- a/USE-CASES.md +++ b/USE-CASES.md @@ -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 @@ -138,3 +137,204 @@ export function run(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([ + 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 From c6cbc9358f44c48b2bec74ad9c089217cc8e8eb5 Mon Sep 17 00:00:00 2001 From: Chengzhong Wu Date: Tue, 9 May 2023 17:10:04 +0800 Subject: [PATCH 2/2] fixup! --- USE-CASES.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/USE-CASES.md b/USE-CASES.md index 7d5a896..202b178 100644 --- a/USE-CASES.md +++ b/USE-CASES.md @@ -289,9 +289,9 @@ 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([ +registerInstrumentations({ instrumentations: [new LongTaskInstrumentation()], -]); +}); // Roughly equals to new PerformanceObserver(list => {...}) .observe({ entryTypes: ['longtask'] }); @@ -320,9 +320,9 @@ 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([ - new FetchInstrumentation(), -]); +registerInstrumentations({ + instrumentations: [new FetchInstrumentation()], +}); // Observes network events and associate them with spans. new PerformanceObserver(list => { const entries = list.getEntries();