Skip to content

Perf/decision task - #1050

Draft
marco-c wants to merge 7 commits into
taskcluster:mainfrom
marco-c:perf/decision-task
Draft

marco-c wants to merge 7 commits into
taskcluster:mainfrom
marco-c:perf/decision-task

Conversation

@marco-c

@marco-c marco-c commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

I haven't yet reviewed all the commits, I'll probably drop some of them, but I want to see if codspeed picks anything up.

create_tasks rescanned every not yet submitted task each time a
createTask call completed, rebuilding its set of dependencies and
checking whether their futures were done. This is quadratic in the
number of tasks, and runs on the main thread while holding the GIL. It
also recursed once per completed batch, so a dependency chain of a few
thousand tasks raised RecursionError.

Now the number of pending dependencies of each task is computed once,
and decremented as dependencies are created. A task is submitted as soon
as its count drops to zero. As before, tasks depending on a task that
failed to be created are not submitted.

With createTask mocked out, creating a 20,000 task graph takes 1.9s
instead of 6.2s (binary tree) and 2.0s instead of 4.6s (fan-out/fan-in).
_recurse walks every task definition when resolving task references
during optimization, and again when resolving timestamps during task
creation. For each single-key dictionary it iterated over all the
parameter functions, building a set of the dictionary keys to compare
against each of them. Now the key is looked up directly in the
parameter functions. Dictionaries are also checked before lists, as
they are much more common in task definitions.

On a typical task definition, resolve_task_references takes 14.5us
instead of 18.3us, and resolve_timestamps 20.5us instead of 23.1us.
`value_of` parsed the same few relative time strings ("1 day",
"28 days", ...) with a regex every time, although task definitions
contain several of them and are resolved multiple times. Its results
are timedeltas, which are immutable, so they are now cached.

On a typical task definition, resolve_timestamps takes 16.4us instead of
20.5us.
Graphs are immutable, but visit_postorder and visit_preorder sorted them
topologically again every time they were called. The full task graph is
visited once per registered verification (11 times in taskgraph alone),
then again to serialize it. The target task graph is visited several
times during optimization. Now the order is computed once per graph and
direction, and cached like links_and_reverse_links_dict already is.

Also, during optimization:
- index paths are gathered by iterating over the tasks directly, as the
  order doesn't matter;
- remove_tasks uses the cached reverse links instead of building them
  again.

On a synthetic graph of 20,210 tasks and 40,200 edges, verifying the
full task graph takes 0.34s instead of 0.73s, and optimizing it takes
1.57s instead of 2.19s.
To decide whether a task can be replaced, replace_tasks computes the
latest deadline of its dependents. It resolved the deadline of every
dependent for every task, although tasks such as docker images or
toolchains share thousands of dependents. Now each task's deadline is
resolved at most once, relative to a single `now`.

Similarly, IndexSearch parsed the same deadline (and the expiration of
tasks used as replacement for multiple tasks) over and over, so parsed
timestamps are now cached.

On a synthetic graph of 20,210 tasks where the 210 build and docker
tasks are replaced, replace_tasks takes 0.17s instead of 0.24s.
The decision task rebuilt Task objects for the whole full task graph
from its JSON representation, only to check that TaskGraph.from_json
works. This is already covered by the TaskGraph tests, and costs time
proportional to the size of the full task graph on every decision task.
Before loading a kind, the generator filtered all the tasks loaded so
far to find the ones belonging to the kind's dependencies (copying them
first when loading kinds in parallel). This is proportional to the
number of kinds times the number of tasks, and in parallel mode it
happens on the main thread, delaying the submission of newly unblocked
kinds.

Now loaded tasks are also grouped by kind, so the tasks of each kind
dependency are looked up directly. They are still passed in the order in
which kinds were loaded.

With 150 kinds of 270 tasks each (40,500 tasks), each depending on three
other kinds, gathering the kind dependency tasks takes 0.01s in total
instead of 0.42s.
@codspeed

codspeed Bot commented Sep 24, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by ×3.3

⚡ 7 improved benchmarks
✅ 17 untouched benchmarks
🆕 4 new benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
⚡ test_for_each_task[diamond] 2,536 ms 62.2 ms ×41
⚡ test_for_each_task[fan] 188.3 ms 62.2 ms ×3
⚡ test_for_each_task[linear] 157.5 ms 61.8 ms ×2.5
⚡ test_for_each_task[btree] 158 ms 62.3 ms ×2.5
⚡ test_taskgraph_to_json[fan] 270.8 ms 145.2 ms +86.45%
⚡ test_taskgraph_to_json[btree] 242.8 ms 145 ms +67.46%
⚡ test_taskgraph_to_json[linear] 240.6 ms 145 ms +65.96%
🆕 test_create_tasks[btree] N/A 3.7 s N/A
🆕 test_create_tasks[fan] N/A 3.9 s N/A
🆕 test_resolve_task_references N/A 3.8 s N/A
🆕 test_resolve_timestamps N/A 4.2 s N/A

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing marco-c:perf/decision-task (55031b8) with main (064375f)

Open in CodSpeed

@sylvestre

Copy link
Copy Markdown

wahou

@marco-c

marco-c commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

@codspeedbot run the new benchmarks against the base of pr, and display the performance changes with the head of the PR, with the link to the corresponding CodSpeed compare runs page

@codspeed

codspeed Bot commented Sep 24, 2026

Copy link
Copy Markdown

@codspeedbot run the new benchmarks against the base of pr, and display the performance changes with the head of the PR, with the link to the corresponding CodSpeed compare runs page

To let the performance wizard handle your request, please sign in to CodSpeed at codspeed.io so we can link your GitHub account, then comment again.

@marco-c

marco-c commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

@codspeedbot run the new benchmarks against the base of pr, and display the performance changes with the head of the PR, with the link to the corresponding CodSpeed compare runs page

@codspeed

codspeed Bot commented Sep 24, 2026

Copy link
Copy Markdown

@codspeedbot run the new benchmarks against the base of pr, and display the performance changes with the head of the PR, with the link to the corresponding CodSpeed compare runs page

Only members of the taskcluster organization on CodSpeed can ask the performance wizard for help here. Once you've joined it on CodSpeed, comment again.

@JohanLorenzo

Copy link
Copy Markdown
Contributor

@codspeedbot run the new benchmarks against the base of pr, and display the performance changes with the head of the PR, with the link to the corresponding CodSpeed compare runs page

@codspeed

codspeed Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

@codspeedbot run the new benchmarks against the base of pr, and display the performance changes with the head of the PR, with the link to the corresponding CodSpeed compare runs page

I ran the PR's benchmark suite (test/test_graph_perf.py) on the PR base (064375f) and then on the PR head (55031b8). Both runs used CPU simulation mode in the same environment. On the base run I used the PR's version of test_graph_perf.py, minus the two graph._visit_order.cache_clear() lines, because _visit_order only exists on the head.

Compare runs page: https://app.codspeed.io/taskcluster/taskgraph/compare/6ab631c04cedfe38851b4dfa..6ab6348ca84b5010809f92e6
(base run: 6ab631c0, head run: 6ab6348c)

New benchmarks (base → head)

Benchmark BASE (064375f) HEAD (55031b8) Change
⚡ test_create_tasks[btree] 12.6 s 1.9 s ×6.7
⚡ test_create_tasks[fan] 12.6 s 2.1 s ×6.1
⚡ test_resolve_timestamps 2.61 s 2.08 s +25.6%
test_resolve_task_references 2.11 s 1.84 s ~+15% (CodSpeed counts this as unchanged)

Existing benchmarks, same pair of runs

Benchmark BASE HEAD Change
⚡ test_for_each_task[diamond] 1,213 ms 28.1 ms ×43
⚡ test_for_each_task[fan] 91.4 ms 28.1 ms ×3.2
⚡ test_for_each_task[btree] 75.3 ms 28.1 ms ×2.7
⚡ test_for_each_task[linear] 75.3 ms 28.2 ms ×2.7
⚡ test_taskgraph_to_json[fan] 136.5 ms 73.6 ms +85.5%
⚡ test_taskgraph_to_json[btree] 122.0 ms 73.3 ms +66.5%
⚡ test_taskgraph_to_json[linear] 121.2 ms 73.4 ms +65.2%

The other 17 benchmarks (links_dict, transitive_closure, visit_*order, transform_sequence) didn't change.

How to read these numbers

  • These runs were in the CodSpeed sandbox, not CI. Absolute values are lower than in the PR report. For example, test_for_each_task[diamond] is 1.2 s here and 2.5 s in CI. Compare base and head within one table, not against the PR report. The ratios match what CI reported for the benchmarks both sets have.
  • test_create_tasks spends most of its time in syscalls. Most of that is thread-pool waiting: about 11,000–12,600 s of system time on base, 2,000–2,700 s on head. Simulation mode leaves syscalls out of the measured value, so the ×6–7 figure covers CPU work only and understates the real cost on both sides. The system-time numbers point the same way: the head waits much less.
  • The environment-difference warning on the compare page doesn't matter here. It comes from the two virtualenvs sitting at different paths. The libraries have identical build IDs.

I didn't change anything on the PR branch.

Summary
  • Warming up the performance wizard...
  • Inspect the new benchmarks and CI setup — Copied the PR's test/test_graph_perf.py onto the base commit (without the _visit_order.cache_clear() calls, which only exist on the PR head)
  • Run the new benchmarks on the PR base (064375f) — Run 6ab631c04cedfe38851b4dfa
  • Run the benchmarks on the PR head (55031b8) — Run 6ab6348ca84b5010809f92e6
  • Compare runs and report — Compare runs and report
  • Handling comment — Comment handling finished successfully

Was this response helpful? React with 👍 or 👎 to help us improve CodSpeedBot.

@adriencaccia

Copy link
Copy Markdown

Hey @marco-c, @JohanLorenzo, the correct compare page URL is: https://app.codspeed.io/taskcluster/taskgraph/runs/compare/6ab631c04cedfe38851b4dfa..6ab6348ca84b5010809f92e6
We will deploy a fix to ensure that the agent will get it right.

This branch has not been deployed

No deployments
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

Successfully merging this pull request may close these issues.

4 participants