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

[RFC] task_group_dynamic_dependencies #1469

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion rfcs/archived/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ two reasons:

1. The document describes a feature or extension that has been deprecated and
then removed.
2. The document describes a proposed feature or extension that have
2. The document describes a proposed feature or extension that has
not (ultimately) become a fully supported feature.

Design documents that appear in the `rfcs/archived` folder should describe a
Expand Down
365 changes: 365 additions & 0 deletions rfcs/proposed/task_group_dynamic_dependencies/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,365 @@
# Extending task_group to manage dynamic dependencies between tasks
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Extending task_group to manage dynamic dependencies between tasks
# Extend ``task_group`` for Dynamic Task Dependencies


## Introduction

Back in 2021, during the move from TBB 2020 to the first release of oneTBB,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Back in 2021, during the move from TBB 2020 to the first release of oneTBB,
In 2021, with the trasition from TBB 2020 to the first release of oneTBB,

the lowest level tasking interface changed significantly and was no longer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
the lowest level tasking interface changed significantly and was no longer
the lowest-level tasking interface changed significantly and was no longer

promoted as a user-facing feature. Instead, the guidance since then has been
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
promoted as a user-facing feature. Instead, the guidance since then has been
promoted as a user-facing feature. Instead, we encouraged

to use the `task_group` or the flow graph APIs to express patterns that were
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
to use the `task_group` or the flow graph APIs to express patterns that were
to use the `task_group` or the flow graph APIs to express patterns

previously expressed using with the lowest level tasking API. And for most
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
previously expressed using with the lowest level tasking API. And for most
previously handled by the lowest-level tasking API. While this approach has been

cases, this has been sufficient. However, there is one use case which is not
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
cases, this has been sufficient. However, there is one use case which is not
sufficient for most cases, one scenario remains challenging: dynamic task graphs that are not trees.

straightforward to express by the revised API: Dynamic task graphs which are
not trees. This proposal expands `tbb::task_group` to make additional use cases
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
not trees. This proposal expands `tbb::task_group` to make additional use cases
not trees. This proposal expands `tbb::task_group` to support additional use cases.

easier to express.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
easier to express.


The class definition from section **[scheduler.task_group]** in the oneAPI
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The class definition from section **[scheduler.task_group]** in the oneAPI
The class definition for `tbb::task_group` from section **[scheduler.task_group]** of the oneAPI

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make it a hyperlink to the specs

Threading Building Blocks (oneTBB) Specification 1.3-rev-1 for `tbb::task_group`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Threading Building Blocks (oneTBB) Specification 1.3-rev-1 for `tbb::task_group`
Threading Building Blocks (oneTBB) Specification 1.3-rev-1

is shown below. Note the existing `defer` function because this function and
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
is shown below. Note the existing `defer` function because this function and
is shown below. Note the existing `defer` function, since this function and

its return type, `task_handle`, are the foundation of our proposed extensions:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
its return type, `task_handle`, are the foundation of our proposed extensions:
its return type, `task_handle`, are the foundation of the proposed extensions:


class task_group {
public:
task_group();
task_group(task_group_context& context);

~task_group();

template<typename Func>
void run(Func&& f);

template<typename Func>
task_handle defer(Func&& f);
vossmjp marked this conversation as resolved.
Show resolved Hide resolved

void run(task_handle&& h);

template<typename Func>
task_group_status run_and_wait(const Func& f);

task_group_status run_and_wait(task_handle&& h);

task_group_status wait();
void cancel();
};

## Proposal

The following table summarizes the three primary extensions that are under
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The following table summarizes the three primary extensions that are under
The following list summarizes the three primary extensions under

consideration. The remainder of this post provides background and further
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
consideration. The remainder of this post provides background and further
consideration. The sections that follow provide background and further

clarification on these proposed extensions.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
clarification on these proposed extensions.
clarification on the proposed extensions.


1. Extend semantics and useful lifetime of `task_handle`. We propose `task_handle`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
1. Extend semantics and useful lifetime of `task_handle`. We propose `task_handle`
1. **Extend the semantics and useful lifetime of `task_handle`.** We propose extending `task_handle`

to represent tasks for the purpose of adding dependencies. The useful lifetime and
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
to represent tasks for the purpose of adding dependencies. The useful lifetime and
to represent tasks for adding dependencies. It requires extending its useful lifetime and semantics

semantics of `task_handle` will need to be extended to include tasks that have been
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
semantics of `task_handle` will need to be extended to include tasks that have been
to include submitted, currently executed, or completed tasks.

submitted, are currently executing, or have been completed.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
submitted, are currently executing, or have been completed.

2. Add functions to set task dependencies. In the current `task_group`, tasks can
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
2. Add functions to set task dependencies. In the current `task_group`, tasks can
2. **Add functions to set task dependencies.** In the current `task_group`, tasks can

only be waited for as a group and there is no direct way to add any before-after
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
only be waited for as a group and there is no direct way to add any before-after
only be waited on as a group, with no direct way to define before-after

relationships between individual tasks. We will discuss options for spelling.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
relationships between individual tasks. We will discuss options for spelling.
relationships between individual tasks. We will explore options for adding such functionality.

3. Add a function to move successors from a currently executing task to a new task.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
3. Add a function to move successors from a currently executing task to a new task.
3. **Add a function to move successors from an executing task to a new task.**

This functionality is necessary for recursively generated task graphs. This case
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This functionality is necessary for recursively generated task graphs. This case
This functionality is necessary for recursively generated task graphs. It enables

represents a situation where it is safe to modify dependencies for an already
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
represents a situation where it is safe to modify dependencies for an already
safe modification of dependencies for an already

submitted task.

### Extend the semantics and useful lifetime of task_handle
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
### Extend the semantics and useful lifetime of task_handle
### Extend The Semantics and Useful Lifetime of ``task_handle``


Dynamic tasks graphs order the execution of tasks via dependencies on the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Dynamic tasks graphs order the execution of tasks via dependencies on the
Dynamic tasks graphs control task execution trough dependencies on the

completion of other tasks. They are dynamic because the creation of tasks,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
completion of other tasks. They are dynamic because the creation of tasks,
completion of other tasks. They are considered dynamic because task creation,

specification of dependencies between, submission of tasks for scheduling, and
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
specification of dependencies between, submission of tasks for scheduling, and
dependency specification, task scheduling submission, and

execution of the tasks may happen concurrently and in various orders. Different
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
execution of the tasks may happen concurrently and in various orders. Different
task execution may happen concurrently and in various orders. Different

concrete use cases have different requirements on when new tasks are created
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
concrete use cases have different requirements on when new tasks are created
use cases have different requirements on when new tasks are created

and when dependencies between tasks are specified.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
and when dependencies between tasks are specified.
and dependencies are specified.


For the sake of discussion, let’s label four points in a task’s lifetime:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
For the sake of discussion, let’s label four points in a task’s lifetime:
To facilitate the discussion, let's define four key points in a task’s lifetime:


1. created
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
1. created
1. **Created:** is allocated but not yet known to the scheduling algorithm, therefore cannot begin executing.

2. submitted
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
2. submitted
2. **Submitted:** is known to the scheduling algorithm and may be scheduled for execution once all of its incoming dependencies (predecessor tasks) are complete.

3. executing
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
3. executing
3. **Executing:** starts executing its body but is not yet complete.

4. completed
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
4. completed
4. **Completed:** fully executed to completion.


A created task has been allocated but is not yet known to the scheduling
algorithm and so cannot begin executing. A submitted task is known to the
scheduling algorithm and whenever its incoming dependencies (predecessor tasks)
are complete it may be scheduled for execution. An executing task has started
executing its body but is not yet complete. Finally, a completed task has
executed fully to completion.

In the current specification for `task_group`, the function `task_group::defer`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
In the current specification for `task_group`, the function `task_group::defer`
In the current `task_group` specification, the `task_group::defer` function

already provides a mechanism to separate task creation from submission.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
already provides a mechanism to separate task creation from submission.
allows to separate task creation from submission.

`task_group::defer` returns a `tbb::task_handle`, which represents a created
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`task_group::defer` returns a `tbb::task_handle`, which represents a created
`task_group::defer` returns a `tbb::task_handle` that represents a created

task. A created task is in the created state until it is submitted via
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
task. A created task is in the created state until it is submitted via
task. A created task remains created until it is submitted through

the `task_group::run` or `task_group::run_and_wait` functions. In the current
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
the `task_group::run` or `task_group::run_and_wait` functions. In the current
`task_group::run` or `task_group::run_and_wait`. The current

specification of `task_group`, accessing a `task_handle` after it is submitted
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
specification of `task_group`, accessing a `task_handle` after it is submitted
`task_group specification treats accessing `task_handle` after it is submitted

via one of the run functions is undefined behavior. Currently, therefore, a
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
via one of the run functions is undefined behavior. Currently, therefore, a
via one of the run functions as an undefined behavior. Therefore,

`task_handle` can only represent a created task. And currently, any task
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`task_handle` can only represent a created task. And currently, any task
`task_handle` represents a created task only. Furthermore, since `task_group` does not support task dependency,

that is run can immediately be scheduled for execution since there is no notion
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
that is run can immediately be scheduled for execution since there is no notion
any task that is run can be immediately scheduled for execution without considering dependencies.

of task dependencies for task_group.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
of task dependencies for task_group.


The first extension is to expand the semantics and usable lifetime of
`task_handle` so that remains valid after it is passed to run and it can
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`task_handle` so that remains valid after it is passed to run and it can
`task_handle` so that it remains valid after being passed to run. This allows it to

represent tasks in any state, including submitted, executing, and completed
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
represent tasks in any state, including submitted, executing, and completed
to represent tasks in any state.

tasks. Similarly, a `task_handle` in the submitted state may represent a task
Comment on lines +96 to +99
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will be necessary to specify in more detail what that means for a task handle to be valid and represent a task in a certain state. As a C++ object, it is valid until it is destroyed, and nothing changes in this regard I guess. But what you can and cannot do with a task handle might depend on the current state of the associated task.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
tasks. Similarly, a `task_handle` in the submitted state may represent a task
tasks. Similarly, submitted `task_handle` may represent a task

that has predecessors that must complete before it can execute, and so passing
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
that has predecessors that must complete before it can execute, and so passing
with predecessors that must complete before it can execute. In that case, passing

a `task_handle` to `task_group::run` or `task_group::run_and_wait` only makes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
a `task_handle` to `task_group::run` or `task_group::run_and_wait` only makes
`task_handle` to `task_group::run` or `task_group::run_and_wait` only makes

it available for dependency tracking, and does not make it immediately legal to
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
it available for dependency tracking, and does not make it immediately legal to
it available for dependency tracking and does not make it immediately eligible for

execute.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
execute.
execution.


### Add function(s) to set dependencies.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
### Add function(s) to set dependencies.
### Add Functions to Set Dependencies


The obvious next extension is to add a mechanism for specifying dependencies
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The obvious next extension is to add a mechanism for specifying dependencies
The next logical extension is to add a mechanism for specifying dependencies

between tasks. In the most conservative view, it should only be legal to add
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
between tasks. In the most conservative view, it should only be legal to add
between tasks. In the most conservative view, it should only be allowed to add

additional predecessors / in-dependencies to tasks in the created state.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
additional predecessors / in-dependencies to tasks in the created state.
additional predecessors (in-dependencies) to tasks in the created state.

After a task starts is completed, it doesn’t make sense to add additional
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
After a task starts is completed, it doesn’t make sense to add additional
After a task is completed, adding more

predecessors, since it’s too late for them to delay the start of the task’s
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
predecessors, since it’s too late for them to delay the start of the task’s
predecessors is irrelevant, since it is too late to delay the task

execution.

It can make sense to add additional predecessors to a task that is
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
It can make sense to add additional predecessors to a task that is
It might be logical to add additional predecessors to a task that is

currently executing if the executing task is suspended until those
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
currently executing if the executing task is suspended until those
currently executing if it is suspended until these

additional dependencies complete. However, in this proposal we do not intend
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
additional dependencies complete. However, in this proposal we do not intend
additional dependencies complete. However, this proposal does not include

to support this suspension model.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
to support this suspension model.
support for the suspension model.


For a task in the submitted state, there can be a race between
adding a new predecessor and the scheduler deciding to execute the task when its
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
adding a new predecessor and the scheduler deciding to execute the task when its
adding a new predecessor and the scheduler deciding to execute the task once its

currently known predecessors have completed. We will revisit the discussion of
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
currently known predecessors have completed. We will revisit the discussion of
current predecessors are complete. We will revisit the discussion of

adding predecessors to submitted tasks in the next section when we discuss
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
adding predecessors to submitted tasks in the next section when we discuss
adding predecessors to submitted tasks in the next section when discussing

recursively grown task graphs.

Having mostly settled the question about when a predecessors can be added,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Having mostly settled the question about when a predecessors can be added,
After resolving the question about when to add predecessors,

then next question is what can be added as a predecessor task? The most
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
then next question is what can be added as a predecessor task? The most
the next question is, what can be added as a predecessor task? The simplest

user-friendly answer is to have no limitation; any valid `task_handle` can act as
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
user-friendly answer is to have no limitation; any valid `task_handle` can act as
answer is to have no limitations. It means, any valid `task_handle` can act as

a predecessor. In many cases, a developer may only know what work must be completed
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
a predecessor. In many cases, a developer may only know what work must be completed
a predecessor. In many cases, you may only know what work must be completed

before a task can start but does not know the state of that work.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
before a task can start but does not know the state of that work.
before a task can start, but you may not know the work state.


We therefore think predecessors may be in any state when they are added,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
We therefore think predecessors may be in any state when they are added,
Therefore, we think that predecessors may be in any state when they are added,

as shown below:

<img src="add_dependency.png" width=400>

There are a number of possible options for the spelling of a function for adding
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
There are a number of possible options for the spelling of a function for adding
There are several options to spell a function for adding

a single predecessor. We may also want a function to allow adding multiple
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
a single predecessor. We may also want a function to allow adding multiple
a single predecessor. Additionally, we may want a function to allow adding multiple

predecessors in one call.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
predecessors in one call.
predecessors in a single call.


Given two `task_handle` objects `h1` and `h2`, some possible options
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Given two `task_handle` objects `h1` and `h2`, some possible options
Given two `task_handle` objects, `h1` and `h2`, some possible options

for adding `h1` as an in-dependence / predecessor of `h2` include:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for adding `h1` as an in-dependence / predecessor of `h2` include:
for adding `h1` as a predecessor (in-dependence) of `h2` include:


- `h2.add_predecessor(h1)`
- `h2 = defer([]() { … }, h1)`
- `make_edge(h1, h2)`

We propose including the first option. Similarly, there could be
Comment on lines +140 to +147
Copy link
Contributor

@akukanov akukanov Dec 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer not to add methods to task_handle but use "external" functions, perhaps in the task_group class. This would be more consistent with the current approach (defer, run, run_and_wait) as well as with transfer_successors_to.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
We propose including the first option. Similarly, there could be
The proposal is to include the first option. Similarly, there could be

versions of these two functions the accepted multiple predecessors
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
versions of these two functions the accepted multiple predecessors
versions of these two functions that accept multiple predecessors

at once:

- `h.add_predecessors(h1, ..., hn)`

In the general case, it would be undefined behavior to add a new predecessor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
In the general case, it would be undefined behavior to add a new predecessor
Generally, it is an undefined behavior to add a new predecessor

to a task in the submitted, executing or completed states.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
to a task in the submitted, executing or completed states.
to a task in the submitted, executing, or completed states.


### Add a function for recursively grown graphs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
### Add a function for recursively grown graphs
### Add Function for Recursively Grown Graphs


A very common use case for oneTBB tasks is parallel recursive decomposition.
The implementation of tbb::parallel_for is an example of an algorithm that
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The implementation of tbb::parallel_for is an example of an algorithm that
An example of this is the implementation of `tbb::parallel_for` that

performs a parallel recursive decomposition of the Range. We currently
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
performs a parallel recursive decomposition of the Range. We currently
performs a parallel recursive decomposition of a range. Currently,

implement the oneTBB algorithms, such as tbb::parallel_for, using the non-public,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
implement the oneTBB algorithms, such as tbb::parallel_for, using the non-public,
the oneTBB algorithms, such as tbb::parallel_for, are implemented using the non-public,

low-level tasking API, not tbb::task_group. The current low-level tasking API
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
low-level tasking API, not tbb::task_group. The current low-level tasking API
low-level tasking API, rather than `tbb::task_group`. This low-level tasking API

puts all the burden on developers for both dependence tracking and memory
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
puts all the burden on developers for both dependence tracking and memory
puts the responsibility for dependence tracking and memory

management of tasks. This lets the TBB development team build highly optimized
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
management of tasks. This lets the TBB development team build highly optimized
management of tasks on developers. While it allows the oneTBB development team to build highly optimized

algorithms, but we believe a simpler set of interfaces are possible for TBB
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
algorithms, but we believe a simpler set of interfaces are possible for TBB
algorithms, a simpler set of interfaces are can be provided for the

users. Recursive parallel algorithms are one of the primary cases that we want
our task_group extension to cover.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
our task_group extension to cover.
`task_group` extension to cover.


The key capability required for recursive decomposition is the ability to
create work while executing a task and insert this newly created work before
the (perhaps already submitted) successors of the currently executing task.
As a simple example, consider a merge sort. As shown in the figure that
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
As a simple example, consider a merge sort. As shown in the figure that
A simple example is a merge sort. As shown in the figure

follows, the top-level algorithm breaks a collection into two pieces and
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
follows, the top-level algorithm breaks a collection into two pieces and
below, the top-level algorithm breaks a collection into two pieces and

creates three tasks:

1. a task to sort the left half
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
1. a task to sort the left half
1. A task to sort the left half.

2. a task to sort the right half
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
2. a task to sort the right half
2. A task to sort the right half.

3. a task to merge the left and right sorted halves once they have been sorted.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
3. a task to merge the left and right sorted halves once they have been sorted.
3. A task to merge the halves once they are sorted.


In a recursive merge sort, each of the sort tasks recursively takes the same
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
In a recursive merge sort, each of the sort tasks recursively takes the same
In a recursive merge sort, each sorting tasks recursively applies the same

approach to sort their portions of the collection. The top-level task (and
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
approach to sort their portions of the collection. The top-level task (and
approach to sort its portions of the collection. The top-level task (and

subsequent recursively generated tasks) must be able to create new tasks
and then update the graph so that their outer merge task waits for the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
and then update the graph so that their outer merge task waits for the
and then update the graph for their outer merge task to wait for the

newly created subtasks to complete.

<img src="merge_sort.png" width=800>

A key point about this recursive parallel algorithm is that we must change
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
A key point about this recursive parallel algorithm is that we must change
A key point of this recursive parallel algorithm is the requirement to change

the predecessors of the merge tasks. But the merge tasks are already
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
the predecessors of the merge tasks. But the merge tasks are already
the predecessors of the merge tasks. However, the merge tasks are already

submitted at the time their predecessors are modified! In the previous
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
submitted at the time their predecessors are modified! In the previous
submitted when their predecessors are modified. As mentioned in the previous

section, we noted that updating the predecessors of a submitted task is
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
section, we noted that updating the predecessors of a submitted task is
section, updating the predecessors of a submitted task can be

risky, because there is a potential race. However, in the example shown
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
risky, because there is a potential race. However, in the example shown
risky due to the potential for a race condition. However, in this case,

here, we know it’s safe to add additional predecessors to the merge task
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
here, we know it’s safe to add additional predecessors to the merge task
it is safe to add additional predecessors to the merge task.

because it simply cannot start executing until all its current predecessors
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
because it simply cannot start executing until all its current predecessors
This is because the merge task cannot start execution until all of its current predecessors

complete, and its predecessors are the tasks modifying the predecessors!
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
complete, and its predecessors are the tasks modifying the predecessors!
complete. Those predecessors are the tasks responsible for modifying the predecessors.


We therefore propose a very limited extension that allows the transfer of
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
We therefore propose a very limited extension that allows the transfer of
Therefore, the proposal is a limited extension that allows transferring

all the successors of a currently executing task to become the successors
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
all the successors of a currently executing task to become the successors
all the successors of a currently executing task to become successors

of a different created task. This function can only access the successors
of the currently executing task, and those tasks are prevented from executing
by a dependence on the current task itself, so we can ensure that we can safely
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
by a dependence on the current task itself, so we can ensure that we can safely
due to dependence on the current task itself. This ensures that it is safe to

update the incoming dependencies for those tasks without worrying about any
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
update the incoming dependencies for those tasks without worrying about any
update the incoming dependencies for those tasks without worrying about

potential race.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
potential race.
potential race conditions.


One possible spelling for this function would be `transfer_successors_to(h)`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
One possible spelling for this function would be `transfer_successors_to(h)`,
The possible spelling for this function is `transfer_successors_to(h)`.

where `h` is a `task_handle` to a created task and the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
where `h` is a `task_handle` to a created task and the
Where `h` is a `task_handle` to a created task, and the

`transfer_successors_to` function must be called from within a task. Calling
this function from outside a task, or passing anything other than a `task_handle`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this function from outside a task, or passing anything other than a `task_handle`
this function from outside a task or passing anything other than a `task_handle`

representing a task in the created state is undefined behavior.

### Proposed changes to task_handle
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
### Proposed changes to task_handle
### Proposed Changes for ``task_handle``


namespace oneapi {
namespace tbb {
class task_handle {
public:

// existing functions
task_handle();
task_handle(task_handle&& src);
~task_handle();
task_handle& operator=(task_handle&& th);
explicit operator bool() const noexcept;

// proposed additions
void add_predecessor(task_handle& th);
void add_successor(task_handle& th);
Comment on lines +226 to +227
Copy link
Contributor

@akukanov akukanov Dec 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we speak about the minimally required API, I doubt we need two variants of the same function, just with different name and the order of arguments. I would only keep one.
See also another comment about functions vs. class methods.

};

void transfer_successors_to(task_handle& th);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this method is related to the currently executing task, what about including this API into tbb::this_task:: namespace? By analogy with tbb::this_task_arena:: namespace.

Copy link
Contributor

@akukanov akukanov Dec 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use tbb::task namespace, since it is already used for suspend (which applies to the currently running task) and resume functions.
Actually no, I think it should not be in the namespace task or this_task or just tbb, but rather it should be a static function or a member function in task_group. The reason is that, since task_group::defer is the only way to create a new non-empty task_handle, the method to transfer successors cannot be used in arbitrary tasks, only in task_group tasks.

}
}


#### void task_handle::add_predecessor(task_handle& th);

Adds `th` as a predecessor that must complete before the task represented by
`*this` can start executing.

#### void task_handle::add_successor(task_handle& th);

Adds `th` as a successor that cannot start executing until the task represented by
`*this` is complete.

#### void transfer_successors_to(task_handle& th);

Transfers all of the successors from the currently executing task to the task
represented by `th`.

### Small examples
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
### Small examples
### Examples


##### A simple three task graph
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
##### A simple three task graph
##### Simple Three-Nodes Task Graph


The example below shows a very simple graph with three nodes. The
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The example below shows a very simple graph with three nodes. The
The example below shows a simple graph with three nodes.

`final_task` must wait for both the `first_task` and `second_task`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`final_task` must wait for both the `first_task` and `second_task`
`final_task` must wait for `first_task` and `second_task`

to complete.

tbb::task_group tg;

tbb::task_handle first_task = tg.defer([&] { /* task body */ });
tbb::task_handle second_task = tg.defer([&] { /* task body */ });
tbb::task_handle final_task = tg.defer([&] { /* task body */ });

final_task.add_predecessor(first_task);
final_task.add_predecessor(second_task);
// optionally: final_task.add_predecessors(first_task, second_task);

// order of submission is not important
tg.run(final_task);
tg.run(first_task);
tg.run(second_task);

tg.wait();

The dependency graph for this example is:

<img src="three_task_graph.png" width=400>

#### Adding predecessors in unknown states
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#### Adding predecessors in unknown states
#### Predecessors in Unknown States


The example below shows a graph where the dependencies are determined
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The example below shows a graph where the dependencies are determined
The example below shows a graph with the dynamically determined dependencies.

dynamically. The state of the predecessors may be unknown – they may
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
dynamically. The state of the predecessors may be unknown – they may
The state of the predecessors is unknown.

be created, submitted, executing or completed. Although not shown,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
be created, submitted, executing or completed. Although not shown,
Assuming that

let's assume that the user's `users::find_predecessors` function
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let's assume that the user's `users::find_predecessors` function
the `users::find_predecessors` function

returns, based on application logic, the tasks that must complete
before the new work can start.

void add_another_task(tbb::task_group& tg, int work_id) {
tbb::task_handle new_task = tg.defer([=] { do_work(work_id); });

for (tbb::task_handle& p : users::find_predecessors(work_id)) {
new_task.add_predecessor(p);
}

tg.run(new_task);
}

Again, the graph, as shown below, is simple. However, now we do not
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Again, the graph, as shown below, is simple. However, now we do not
While the graph, as shown below, is simple,

know the completion status of the predecessors. Therefore,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
know the completion status of the predecessors. Therefore,
the completion status of the predecessors is unknown. Therefore,

for ease-of-use, a `task_handle` should be usable as a dependency
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for ease-of-use, a `task_handle` should be usable as a dependency
for ease of use, `task_handle` should be usable as a dependency

regardless of state of the task it represents. Any predecessor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
regardless of state of the task it represents. Any predecessor
regarding the task state. Any predecessor

that is already completed when it is added as a predecessor will
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
that is already completed when it is added as a predecessor will
completed when it is added as a predecessor does

not delay the start of the dependent task. Otherwise, end-users
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
not delay the start of the dependent task. Otherwise, end-users
not delay the start of the dependent task. Otherwise, the end users

will need to track these states explicitly.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
will need to track these states explicitly.
do not need to track the states explicitly.


<img src="unknown_states.png" width=400>

#### And example of recursive decomposition
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#### And example of recursive decomposition
#### Recursive Decomposition


This example is a version of merge-sort (with many of the details left out).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This example is a version of merge-sort (with many of the details left out).
This example shows a version of merge sort, with mane details left out for simplicity.

Assume that there is an initial task that executes the function shown
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Assume that there is an initial task that executes the function shown
Assume an initial task executes the function shown

below as its body, and the function implements that task, and also serves
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
below as its body, and the function implements that task, and also serves
This function implements that task and serves

as the body for the recursively decomposed pieces. The beginning and
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
as the body for the recursively decomposed pieces. The beginning and
as the body for the recursively decomposed pieces.

end of the sequence are represented by `b` and `e`, and much of the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
end of the sequence are represented by `b` and `e`, and much of the
The range of the sequence is defined by `b` (beginning) and `e` (end).

(unimportant) details of the implementation of merge-sort is hidden
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
(unimportant) details of the implementation of merge-sort is hidden
Most of the implementation details of merge sort are abstracted into the following helper functions:

behind the functions `users::do_serial_sort`, `users::create_left_range`,
`users::create_right_range`, and `users::do_merge`.

template<typename T>
void merge_sort(tbb::task_group& tg, T b, T e) {
if (users::range_is_too_small(b, e)) {
// base-case when range is small
users::do_serial_sort(b, e);
} else {
// calculate left and right ranges
T lb, le, rb, re;
users::create_left_range(lb, le, b, e);
users::create_right_range(rb, re, b, e);

// create the three tasks
tbb::task_handle sortleft =
tg.defer([lb, le, &tg] {
merge_sort(tg, lb, le);
});
tbb::task_handle sortright =
tg.defer([rb, re, &tg] {
merge_sort(tg, rb, re);
});
tbb::task_handle merge =
tg.defer([rb, re, &tg] {
users::do_merge(tg, lb, le, rb, re);
});

// add predecessors for new merge task
merge.add_predecessors(sortleft, sortright);

// insert new subgraph between currently executing
// task and its successors
tbb::transfer_successors_to(merge);

tg.run(sortleft);
tg.run(sortright);
tg.run(merge);
}
}

The task tree for this example matches the one shown earlier for merge-sort.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The task tree for this example matches the one shown earlier for merge-sort.
This task tree matches the one shown earlier for merge-sort.


## Open Questions in Design

Some open questions that remain:
Comment on lines +359 to +361
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should there be more implementation-level questions added, such as feasibility of implementing the proposed design, performance implications, etc.?


- Are the suggested APIs sufficient?
- Are there additional use cases that should be considered that we missed in our analysis?
- Are there other parts of the pre-oneTBB tasking API that developers have struggled to find a good alternative for?
Comment on lines +363 to +365
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are interesting questions.

While reading this RFC, I kind of rushed proposing additional syntax sugar that besides being more user-friendly, since it seems to represent popular use cases, can save some CPU cycles. So I am posting them here for a discussion.

  1. Would it be useful to have a method that simultaneously joins (or even fuses?) the instantiation of a task and transferring of the current task successors?
    Something like:
    template <typename Func>
    task_handle transfer_successors_to(Func&& f);

For the recursive decomposition scenario, instantiating a new task within an executing task and transferring successors to that new task seems to be the main model of writing such algorithms. Although, it seems to be not saving much (only one call to the library and assign to a couple of pointers?), there is always(?) going to be such a sequence in the code. Otherwise, how else an execution of already submitted tasks can be postponed?

Shall we also consider a question of having that API instead or in addition to the one proposed?

  1. As for add_predecessor(s) and add_successor(s) I have a couple of thoughts.
    a. It seems again that it might be useful to merge instantiation of the new task handles and adding them as successors/predecessors:
        template <typename Func>
        void add_predecessor(Func&& f);
    
        template <typename Func>
        void add_successor(Func&& f);
    b. Also, I think having an API that would allow adding more than one predecessor/successor at once can be useful, since usually a number of successors/predecessors are instantiated. I only think that we don't need to limit ourselves to only two parameters as it was proposed optionally, but allow passing of an arbitrary size of task handles or even user lambdas. Of course, a pattern of having a single task producer might be viewed as a limiting one, but there actually might be the cases where tasks cannot be made available to the scheduler (i.e. spawned) until all of them are gathered together from different producers, which essentially represents a barrier in the execution pipeline. Not to mention that the spawning of a bunch of tasks all together are done faster than regular one by one spawnings. Spawning of a bunch of tasks at once was implemented in the old TBB, as far as I remember.
    So here I suggest to have something like:
        template <typename... Func>
        void add_predecessors(Func&& ...f);
    
        template <typename... Func>
        void add_successor(Func&& ...f);
    However, this also seems to ask for having the task_group::run() method to accept an arbitrary size of task handles and/or functors. So, perhaps, it is more related to another RFC/extension...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should start with minimal API then extend with syntactic sugar based on use cases. I suspect this will start as an experimental feature to allow some feedback on API.

Even so, I'm also open to including these additional APIs in the initial implementation, if others think they're likely needed.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading