Skip to content

Commit 7c6d1e1

Browse files
committed
Update job queue documentation
1 parent 1183fd3 commit 7c6d1e1

File tree

5 files changed

+182
-154
lines changed

5 files changed

+182
-154
lines changed

collector/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ This crate is only compatible with OpenSSL 1.0.1, 1.0.2, and 1.1.0, or LibreSSL
3434
aborting due to this version mismatch.
3535
```
3636

37+
For benchmarking using `perf`, you will also need to set `/proc/sys/kernel/perf_event_paranoid` to `-1`.
38+
3739
## Benchmarking
3840

3941
This section is about benchmarking rustc, i.e. measuring its performance on the

docs/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# rustc-perf documentation
2+
3+
- [Glossary of useful terms](./glossary.md)
4+
- [Database schema](../database/schema.md)
5+
- [How rustc-perf is deployed](./deployment.md)
6+
- [How the distributed job queue works](./job-queue.md)
7+
- [How we compare benchmarks results](./comparison-analysis.md)

docs/glossary.md

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ The following is a glossary of domain specific terminology. Although benchmarks
2525
- `incr-patched`: incremental compilation is used, with a full incremental cache and some code changes made.
2626
* **backend**: the codegen backend used for compiling Rust code.
2727
- `llvm`: the default codegen backend
28+
- `cranelift`: experimental backend designed for quicker non-optimized builds
29+
* **target**: compilation target for which the benchmark is compiled.
30+
- `x86_64-unknown-linux-gnu`: the default x64 Linux target
2831
* **category**: a high-level group of benchmarks. Currently, there are three categories, primary (mostly real-world crates), secondary (mostly stress tests), and stable (old real-world crates, only used for the dashboard).
2932
* **artifact type**: describes what kind of artifact does the benchmark build. Either `library` or `binary`.
3033

@@ -41,15 +44,15 @@ The following is a glossary of domain specific terminology. Although benchmarks
4144
## Testing
4245

4346
* **test case**: a combination of parameters that describe the measurement of a single (compile-time or runtime) benchmark - a single `test`
44-
- For compile-time benchmarks, it is a combination of a benchmark, a profile, and a scenario.
45-
- For runtime benchmarks, it is currently only the benchmark name.
47+
- For compile-time benchmarks, it is a combination of a benchmark, a profile, a scenario, a codegen backend and a target.
48+
- For runtime benchmarks, it a combination of a benchmark and a target.
4649
* **test**: the act of running an artifact under a test case. Each test is composed of many iterations.
4750
* **test iteration**: a single iteration that makes up a test. Note: we currently normally run 3 test iterations for each test.
48-
* **test result**: the result of the collection of all statistics from running a test. Currently, the minimum value of a statistic from all the test iterations is used for analysis calculations and the website.
49-
* **statistic**: a single measured value of a metric in a test result
51+
* **test result**: the set of all gathered statistics from running a test. Currently, the minimum value of a statistic from all the test iterations is used for analysis calculations and the website.
52+
* **statistic**: a single measured value of a metric in a test iteration
5053
* **statistic description**: the combination of a metric and a test case which describes a statistic.
5154
* **statistic series**: statistics for the same statistic description over time.
52-
* **run**: a set of tests for all currently available test cases measured on a given artifact.
55+
* **run**: a set of tests for all currently available test cases measured on a given artifact.
5356

5457
## Analysis
5558

@@ -60,7 +63,17 @@ The following is a glossary of domain specific terminology. Although benchmarks
6063
* **relevant test result comparison**: a test result comparison can be significant but still not be relevant (i.e., worth paying attention to). Relevance is a factor of the test result comparison's significance and magnitude. Comparisons are considered relevant if they are significant and have at least a small magnitude .
6164
* **test result comparison magnitude**: how "large" the delta is between the two test result's under comparison. This is determined by the average of two factors: the absolute size of the change (i.e., a change of 5% is larger than a change of 1%) and the amount above the significance threshold (i.e., a change that is 5x the significance threshold is larger than a change 1.5x the significance threshold).
6265

63-
## Other
66+
## Job queue
67+
68+
These terms are related to the [job queue system](./job-queue.md) that distributes benchmarking jobs across available collectors.
69+
70+
- **benchmark request**: a request for a benchmarking a *run* on a given *artifact*. Can be either created from a try build on a PR, or it is automatically created from merged master/release *artifacts*.
71+
- **collector**: a machine that performs benchmarks.
72+
- **benchmark set**: a subset of a compile/runtime/bootstrap benchmark suite that is executed by a collector in a single job.
73+
- **job**: a high-level "work item" that defines a set of *test cases* that should be benchmarked on a specific collector.
74+
- **job queue**: a queue of *jobs*.
75+
76+
## Other
6477

6578
* **bootstrap**: the process of building the compiler from a previous version of the compiler
6679
* **compiler query**: a query used inside the [compiler query system](https://rustc-dev-guide.rust-lang.org/overview.html#queries).

docs/job-queue.md

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# Job queue
2+
3+
> Before reading this document, please examine the [glossary](./glossary.md), in particular the part about the [job queue](./glossary.md#job-queue).
4+
5+
In addition to simple local execution of benchmarks, `rustc-perf` can also serve as a distributed system that supports benchmarking a single compiler artifact in parallel across several collector (machines) that can even run on various hardware architectures. This distributed system is documented in this file.
6+
7+
Another overview of how the rustc-perf benchmark suite operates can found [here](https://kobzol.github.io/rust/rustc/2023/08/18/rustc-benchmark-suite.html), although that was written before `rustc-perf` supported multiple machines.
8+
9+
## High-level overview
10+
There are two main entities operating in the distributed system:
11+
- The [website](https://perf.rust-lang.org) receives requests for benchmarking a specific version of the compiler, splits those requests into more granular chunks of work ("jobs"), waits until all (non-optional) jobs of a request are completed and then reports the benchmark results on the corresponding pull request via a comment on GitHub.
12+
- A set of collectors (dedicated machines) repeatedly poll for new jobs appearing. After they dequeue a job, they benchmark all test cases from it and store the results into the database.
13+
14+
The website communicates with the collectors through a Postgres database, there is no other external service for managing the job queue.
15+
16+
Let's walk through the distributed system step by step.
17+
18+
## Website
19+
The main goal of the website in the distributed system is to create benchmark requests, split them into jobs, and wait until they are completed. The website has a periodically executed background process ("cron") that checks whether some progress can be made.
20+
21+
### Creating benchmark requests
22+
The main event that starts the whole benchmarking process is the creation of a single benchmark request. Benchmark requests are stored in the database in the `benchmark_request` column.
23+
24+
They come in three types:
25+
26+
- Master: benchmark a commit merged into the `rust-lang/rust`'s default branch.
27+
- Master requests are created in the cron, once new commits appear in `rust-lang/rust`.
28+
- Try: benchmark a try build on a given PR.
29+
- Try requests are created by commands sent in GitHub comments in a PR.
30+
- Release: benchmark a released stable/beta version of Rust.
31+
- Release requests are created in the cron, once a new release is made.
32+
33+
Every benchmark request has a *parent* request, with which its benchmark results will be compared once it is finished. A parent request should generally always be benchmarked before its children.
34+
35+
The benchmark request can be in one of four states, whose state diagram is shown below:
36+
37+
```mermaid
38+
flowchart LR
39+
WA[Waiting for artifacts] -->|Try build finished| AR
40+
AR[Artifacts ready] -->|Jobs enqueued| IP
41+
IP[In progress] --> |Success| Completed
42+
IP --> |Error| Failed
43+
```
44+
45+
Some useful observations:
46+
- Try requests start in the `Waiting for artifacts` state, while master and release requests already start in the `Artifacts ready` state.
47+
- A request cannot start being benchmarked until its compiler artifacts are available on CI.
48+
- Once a request moves into the `Completed` or `Failed` state, its state will never change again.
49+
- New jobs can still be generated for such a request though, see [backfilling](#backfilling) below.
50+
51+
### Benchmark request queue
52+
Since multiple requests can be ready to be benchmarked at any given time, the website orders them in a queue, which is displayed on the [status page].
53+
54+
The ordering of the queue is somewhat ephemeral, as it can change anytime a new benchmark request appears. That being said, the website also tries to avoid "jumps" in the queue when requests are completed, for better predictability of when a given request benchmark will be completed.
55+
56+
The ordering looks approximately like this:
57+
1. `In progress` requests.
58+
2. Release requests, sorted by date and then name.
59+
3. `Artifacts ready` requests. These requests are sorted topologically, with the topological level determined by the transitive number of parents that are not done yet. So requests with a done parent have priority between requests whose parent isn't done yet.
60+
61+
Within the individual groups, requests are ordered by their PR number and creation time.
62+
63+
Currently, the website maintains an invariant that at most a single benchmark request is `In progress`. This means that even if one of the collectors is finished with all its jobs, it will have to wait until all other jobs of the request are complete. This helps us synchronise the benchmarking workload so that it is easier to keep track of what is going on in the system. This constraint could be relaxed in the future, most of the system should be prepared for running multiple requests at the same time. Although if the workflow is well-balanced, it should not be needed.
64+
65+
### Enqueuing jobs
66+
The cron periodically scans the benchmark request queue. Once it sees that no benchmark request is `In progress`, and there is at least a single request that is in the `Artifacts ready` state, it will atomically transition that request to the `In progress` state and enqueue a set of benchmark jobs for the request into the `job_queue` database table.
67+
68+
Each benchmark job describes a subset of test cases of the whole request that will be benchmarked on a single collector. More specifically, it states which test cases (profile, codegen backend and target), benchmark suite (compile, runtime or rustc) and which *benchmark set* (a subset of the compile benchmark suite) should be benchmarked.
69+
70+
The jobs exist so that we can split the request into smaller chunks, which can then be benchmarked in parallel on multiple collectors, thus shortening the whole benchmark run duration. This granularity also allows us to have collectors with different hardware architectures, and support [backfilling](#backfilling).
71+
72+
Each job can exist in the following four states:
73+
```mermaid
74+
flowchart LR
75+
Queued -->|Job dequeued for the first time| IP
76+
IP[In progress] --> |Success| Success
77+
IP[In progress] --> |"Failure (retry<MAX_RETRIES)"| IP
78+
IP[In progress] --> |"Failure (retry==MAX_RETRIES)"| Failure
79+
```
80+
81+
Once the jobs have been enqueued, the website will repeatedly check in the cron whether all (non-optional[^optional]) of the `In progress` request and also all jobs of its parent (see [backfilling](#backfilling)) have been completed. Once that happens, it will then transition the request into the `Completed` or `Failed` state (based on whether there were any failed jobs or not) and send a GitHub pull request comment with the benchmark result (for master and try requests).
82+
83+
[^optional]: Some jobs can be marked as optional; this is designed to allow running experimental collectors that should not "block" the main benchmark request workflow.
84+
85+
#### Benchmark set
86+
Each job contains a specific *benchmark set*, a small integer that identifies which subset of the compile benchmark suite should be benchmarked in the job. This is used to further split the compile benchmark suite, which takes the most time to run, and thus enable parallelizing its execution across multiple collectors.
87+
88+
The compile benchmark suite for a given target is split into `N` benchmark sets. To run the suite, `N` collectors (with benchmark sets `0, 1, ..., N`) have to be available.
89+
90+
Each collector has a hard-coded benchmark set that it always benchmarks. Benchmarks should ideally not move within the sets, to ensure that each benchmark will always be benchmarked on the same machine, to avoid unnecessary environment noise.
91+
92+
That being said, sometimes it might be useful to balance the sets a little bit, to ensure that all collectors can run their jobs in approximately the same duration, to avoid unbalanced workloads.
93+
94+
The fact that the benchmark sets are assigned to collectors *statically* and there is no load balancing or work-stealing going on in-between the sets means that if one of the collectors stops running, **it will halt the whole system**. In that case, a [manual intervention](./deployment.md) might be required.
95+
96+
#### Backfilling
97+
When the website enqueues jobs of a request, it also enqueues the jobs with the same parameters for its parent request. Parents should always be benchmarked before their children, so in most cases, all the parent jobs will already be present in the job queue.
98+
99+
However, someone can create a try request with *non-default* parameters. For example, they could request benchmarking `cranelift` codegen backend, which is not normally benchmarked on master requests. When that happens, we need to ensure that we will also run those non-default jobs for the *parent* in addition to the try request itself, otherwise we wouldn't have anything to compare to.
100+
101+
This situation is called `backfilling`. When the website enqueues jobs for a request with non-default parameters, it will create *new jobs* also for its parent request, which did not exist before. The collectors will then go and also benchmark those parent jobs, thus "backfilling" the results into a request that was already completed previously. The status of the parent request does not change when this happens, it stays `Completed` or `Failed`.
102+
103+
## Collectors
104+
105+
The main job of each collector is to continuously dequeue jobs from the job queue, run all of their benchmarks and store the results into the database.
106+
107+
### Registration
108+
Individual collectors have to be registered in the database, so that we can show their status on the [status page], even if they were offline at the moment.
109+
110+
Their information is stored in the `collector_config` table. Each collector has its assigned benchmark set and target, which are used to determine which jobs the collector will handle.
111+
112+
To register a new collector, you can run the following command:
113+
114+
```bash
115+
cargo run --bin collector add_collector \
116+
--collector_name "<name>" \
117+
--target <target> \
118+
--benchmark_set "<benchmark-set>" \
119+
--is_active
120+
```
121+
122+
Collector names and `(target, benchmark_set)` combinations have to be unique.
123+
124+
If a given collector is not used anymore, it can be marked in the database as being inactive. This currently has to be done manually by modifying the database.
125+
126+
### Dequeuing jobs
127+
To run a collector with a given name, you can run the following command:
128+
```bash
129+
cargo run --bin collector benchmark_job_queue --collector_name "<name>"
130+
```
131+
132+
After starting, the collector will enter a loop in which it will repeatedly (every 30s) poll the `job_queue` table, looking for a job that matches its target and benchmark set. If it finds such a job, it will atomically dequeue it, marking it as being `In progress`, and increasing its [retry counter](#failure-handling-and-retries). Then it will download the compiler artifacts[^artifacts-cache] specified by the job and perform all its test cases.
133+
134+
[^artifacts-cache]: The artifacts are cached on disk, to avoid re-downloading the same artifacts multiple times, as every benchmark request will generate several jobs for each active collector.
135+
136+
### Failure handling and retries
137+
Several kinds of failures can happen during the execution of a job:
138+
- Handled transient failure: it was not possible to download CI artifacts because of a transient network error, or it was not possible to communicate with the DB. In this case, the collector will try to record the error into the `errors` table. The job will not be marked as completed, it will simply be dequeued later again, after a short wait.
139+
- Handled permanent failure: some error that is most likely unrecoverable has happened (for example, CI artifacts for the given compiler `SHA` are not available). In this case, the collector will record the error and immediately mark the job as failed and moves on.
140+
- Unhandled failure (panic): the collector failed unexpectedly and could not record the error (we currently don't catch panics). In this case the `collector` service will restart the collector later, and it will try to dequeue the job again.
141+
142+
If the collector dequeues a job that already has its retry counter set to `MAX_RETRIES` (see [job lifecycle](#enqueuing-jobs) diagram), it will mark the job as failed.
143+
144+
The collector prioritizes continuing `In progress` jobs before starting new `Queued` jobs.
145+
146+
### Automatic git update
147+
The collector is executed through a [bash script](../collector/collect-job-queue.sh), which runs it in a loop (in case it ends or crashes). Before the collector is started, the bash script downloads the latest version of `rustc-perf` from GitHub, and rebuilds the collector, to keep it up to date.
148+
149+
If there are always enough jobs to benchmark, the collector might not exit for some time. The collector thus also checks the latest `rustc-perf` commit SHA while it is running. If it determines that a new version is available, it shuts itself down to let the bash script update it. However, the collector tries to delay the shutdown until after it finishes all jobs of a request that is currently `In progress`, to avoid changing the collector version in a single benchmark run.
150+
151+
### Heartbeat
152+
The collector periodicaly updates its last heartbeat date, which is displayed on the [status page]. When the heartbeat is too old, the collector will be marked as being `Offline`.
153+
154+
[status page]: https://perf.rust-lang.org/status.html

0 commit comments

Comments
 (0)