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

Suggestion: use dedicated syntax #4

Open
dead-claudia opened this issue Mar 26, 2025 · 0 comments
Open

Suggestion: use dedicated syntax #4

dead-claudia opened this issue Mar 26, 2025 · 0 comments

Comments

@dead-claudia
Copy link

dead-claudia commented Mar 26, 2025

Copied from tc39/proposal-async-context#113 with the only change being the title, dynamic using to a more readable with, and me combining the first two comments.

Click to expand I found a way to make everything but the variable get fast in [this comment](https://github.com/tc39/proposal-async-context/issues/107#issuecomment-2622966231), and that one remaining optimization looks an awful lot like the inline caches used for types. The fallback path is about the only part that isn't a trivially inlined function.

And for semantics, there's concerns about excessive creation of async variables, and I do share them: tc39/proposal-async-context#50

One way to quietly guide people to use async vars correctly (and to optimize async var usage in general) is to make them literal variable bindings. Also, admittedly, .get() is annoying boilerplate.

And for performance, this glides right in and makes it all both trivially tree-shakeable and essentially zero-cost.

So, what about this syntax?

The syntax here is intentionally somewhat anti-bikeshed. Concise enough to get the point across, but (especially for snapshots) ugly enough to move discussion along.

// Define one or more dynamic variable
dynamic foo = initialValue, bar = ...

// Get the variable's current value
let currentValue = foo

// Set a dynamic variable in a scope
// Automatically restored after exit
{
    with foo = otherValue
    with path.to.foo = otherValue
    // ...
}

// Get a snapshot of the current state
// Semantically changes on dynamic set, but engines can amortize that
const snapshot = function.context

// Use snapshot
with * from snapshot

From a spec standpoint, this would be a unique type of reference, alongside property values. Module property accesses would return dynamic values as references to dynamic values.


To show how this all would play out in practice, here's most of the README examples:

dynamic asyncVar;

// Sets the current value to 'top', and executes the `main` function.
{
    with asyncVar = "top";
    main()
}

function main() {
  // Dynamic variable is maintained through other platform queueing.
  setTimeout(() => {
    console.log(asyncVar); // => 'top'

    {
      with asyncVar = 'A'
      console.log(asyncVar); // => 'A'

      setTimeout(() => {
        console.log(asyncVar); // => 'A'
      }, randomTimeout());
    }
  }, randomTimeout());

  // Dynamic variable runs can be nested.
  {
    with asyncVar = "B";
    console.log(asyncVar); // => 'B'

    setTimeout(() => {
      console.log(asyncVar); // => 'B'
    }, randomTimeout());
  }

  // Dynamic variable was restored after the previous run.
  console.log(asyncVar); // => 'top'
}

function randomTimeout() {
  return Math.random() * 1000;
}
dynamic asyncVar;

let snapshot;
{
  with asyncVar = "A";
  // Captures the state of all dynamic variables at this moment.
  snapshot = function.context;
}

{
  with asyncVar = "B";
  console.log(asyncVar); // => 'B'

  // The snapshot will restore all dynamic variables to their snapshot
  // state and invoke the wrapped function. We pass a function which it will
  // invoke.
  with * from snapshot;
  // Despite being lexically nested inside 'B', the snapshot restored us to
  // to the snapshot 'A' state.
  console.log(asyncVar); // => 'A'
}
let queue = [];

export function enqueueCallback(cb: () => void) {
  // Each callback is stored with the context at which it was enqueued.
  const snapshot = function.context;
  queue.push({snapshot, cb});
}

runWhenIdle(() => {
  // All callbacks in the queue would be run with the current context if they
  // hadn't been wrapped.
  for (const {snapshot, cb} of queue) {
    with * from snapshot;
    cb();
  }
  queue = [];
});
// tracer.js

dynamic span;
export function run(cb) {
  // (a)
  with span = {
    startTime: Date.now(),
    traceId: randomUUID(),
    spanId: randomUUID(),
  };
  cb();
}

export function end() {
  // (b)
  span?.endTime = Date.now();
}
dynamic currentTask = { priority: "default" };
const scheduler = {
  postTask(task, options) {
    // In practice, the task execution may be deferred.
    // Here we simply run the task immediately.
    with currentTask = { priority: options.priority };
    task();
  },
  currentTask() {
    return currentTask;
  },
};

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

async function task() {
  // Fetch remains background priority by referring to scheduler.currentTask().
  const resp = await fetch("/hello");
  const text = await resp.text();

  scheduler.currentTask(); // => { priority: 'background' }
  return doStuffs(text);
}

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

The choice of keyword could also of course be changed to something like with, like with currentTask = { priority: options.priority };.

But in any case, there's unique advantages to using dedicated syntax:

  • using foo = asyncVar.withValue(new Resource()) precludes stuff like using with foo = new Resource() simultaneously setting the async variable and cleaning it up as it leaves scope.
  • Using static syntax allows engines to batch their insertion and cleanup. If they use arrays instead of linked lists, this could result in significant perf wins in JIT-compiled code.
  • Using static syntax means far fewer inline caches needed. All that's needed is a single type check per variable, something very helpful for high-performance libraries and frameworks that need to set async context in perf-critical spots.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant