Split integration test suites into focused async modules - #6826
Conversation
|
CI failed because with a bit longer app names the Still, we get a preview of how tests get scheduled: Beforegantt
title Module Execution Timeline
dateFormat mm:ss
axisFormat %M:%S
section Modules
UmbrellaAppWithDefaultsTest :crit, active, 00:00, 08:14
AppWithDefaultsTest :active, 00:00, 08:04
AppWithMySqlAdapterTest :active, 00:00, 07:16
AppWithScopesTest :active, 00:00, 06:39
AppWithMSSQLAdapterTest :active, 00:00, 06:27
AppWithSQLite3AdapterTest :active, 00:00, 06:17
AppWithNoOptionsTest :active, 00:00, 01:34
Aftergantt
title Module Execution Timeline
dateFormat mm:ss
axisFormat %M:%S
section Modules
UmbrellaAppWithDefaultsAuthTest :crit, active, 02:21, 07:00
AppWithMySqlAdapterAuthTest :active, 02:37, 07:11
AppWithSQLite3AdapterAuthTest :active, 00:00, 04:31
AppWithMSSQLAdapterAuthTest :active, 03:09, 07:21
AppWithScopesJsonTest :active, 04:29, 07:01
AppWithDefaultsAuthTest :active, 05:22, 07:53
AppWithDefaultsHtmlTest :active, 04:31, 06:44
AppWithDefaultsJsonTest :active, 00:00, 02:12
UmbrellaAppWithDefaultsLiveTest :active, 00:00, 02:10
UmbrellaAppWithDefaultsHtmlTest :active, 00:00, 02:10
AppWithDefaultsLiveTest :active, 00:00, 02:08
UmbrellaAppWithDefaultsJsonTest :active, 01:08, 03:09
AppWithNoOptionsTest :active, 05:08, 07:02
UmbrellaAppWithDefaultsTest :active, 02:10, 04:02
AppWithScopesLiveTest :active, 00:00, 01:41
AppWithDefaultsTest :active, 05:32, 07:11
AppWithScopesHtmlTest :active, 03:43, 05:22
AppWithMySqlAdapterScopesTest :active, 02:10, 03:43
AppWithScopesCustomRoutesTest :active, 02:08, 03:29
AppWithMySqlAdapterLiveTest :active, 00:00, 01:19
AppWithMSSQLAdapterJsonTest :active, 04:22, 05:32
AppWithSQLite3AdapterHtmlTest :active, 00:00, 01:08
AppWithSQLite3AdapterJsonTest :active, 03:14, 04:22
AppWithMSSQLAdapterLiveTest :active, 04:02, 05:08
AppWithMSSQLAdapterHtmlTest :active, 01:19, 02:21
AppWithMySqlAdapterHtmlTest :active, 02:12, 03:14
AppWithSQLite3AdapterLiveTest :active, 03:29, 04:29
AppWithMySqlAdapterJsonTest :active, 01:41, 02:37
(the "critical path" highlighting is probably broken, my bad...) |
I overcame this by methodically naming the apps (same name per module) and also naming the modules in a methodical way, which makes it easier to see the gaps in test coverage. |
a0e9629 to
1869ac2
Compare
|
@SteffenDE as a follow up we can then use test partitions to utilize even more cores in parallel. Each job gets 8 vCPUs, and the repo gets up-to 20 concurrent jobs (which we saturate using a matrix of elixir/otp versions and partition numbers). |
1869ac2 to
b84956c
Compare
|
Interesting, so the overall runtime seems pretty much the same? Did you try increasing max_cases? |
b84956c to
d5b752d
Compare
Yes, the gain is not yet meaningful, though we have better CPU utilization. Here's the execution chart of that old run (#6826 (comment)) organized into 8 lanes: ---
displayMode: compact
---
gantt
title Module Execution Timeline (8 Concurrent Lanes)
dateFormat mm:ss
axisFormat %M:%S
todayMarker off
section Lane 1
AppWithSQLite3AdapterAuthTest :active, 00:00, 04:31
AppWithDefaultsHtmlTest :active, 04:31, 06:44
section Lane 2
AppWithDefaultsJsonTest :active, 00:00, 02:12
AppWithMySqlAdapterHtmlTest :active, 02:12, 03:14
AppWithSQLite3AdapterJsonTest :active, 03:14, 04:22
AppWithMSSQLAdapterJsonTest :active, 04:22, 05:32
AppWithDefaultsTest :active, 05:32, 07:11
section Lane 3
UmbrellaAppWithDefaultsLiveTest :active, 00:00, 02:10
UmbrellaAppWithDefaultsTest :active, 02:10, 04:02
AppWithMSSQLAdapterLiveTest :active, 04:02, 05:08
AppWithNoOptionsTest :active, 05:08, 07:02
section Lane 4
UmbrellaAppWithDefaultsHtmlTest :crit, active, 00:00, 02:10
AppWithMySqlAdapterScopesTest :crit, active, 02:10, 03:43
AppWithScopesHtmlTest :crit, active, 03:43, 05:22
AppWithDefaultsAuthTest :crit, active, 05:22, 07:53
section Lane 5
AppWithDefaultsLiveTest :active, 00:00, 02:08
AppWithScopesCustomRoutesTest :active, 02:08, 03:29
AppWithSQLite3AdapterLiveTest :active, 03:29, 04:29
AppWithScopesJsonTest :active, 04:29, 07:01
section Lane 6
AppWithScopesLiveTest :active, 00:00, 01:41
AppWithMySqlAdapterJsonTest :active, 01:41, 02:37
AppWithMySqlAdapterAuthTest :active, 02:37, 07:11
section Lane 7
AppWithMySqlAdapterLiveTest :active, 00:00, 01:19
AppWithMSSQLAdapterHtmlTest :active, 01:19, 02:21
UmbrellaAppWithDefaultsAuthTest :active, 02:21, 07:00
section Lane 8
AppWithSQLite3AdapterHtmlTest :active, 00:00, 01:08
UmbrellaAppWithDefaultsJsonTest :active, 01:08, 03:09
AppWithMSSQLAdapterAuthTest :active, 03:09, 07:21
Running 4 of the new modules sequentially yielded a similar time as the previous longest running test module.
Not yet. Have focused on correctness, first gain confidence we're not accidentally changing tests in unintended ways. I think now we're in a good spot to experiment further. |
I think it will not be useful because each test runs CPU bound tasks (compilation), we'll probably overbook the 4 vCPUs. But let's try it anyway, will send it in a separate commit we can decide to keep or drop. |
2017106 to
ece9aed
Compare
|
https://github.com/phoenixframework/phoenix/actions/runs/33566236525/attempts/1#summary-100049951994 ---
displayMode: compact
---
gantt
title Module Execution Timeline
dateFormat mm:ss
axisFormat %M:%S
todayMarker off
section Lane 1
PostgresHtml :active, 00:00, 03:10
PostgresJson :active, 03:10, 05:53
section Lane 2
PostgresLive :active, 00:00, 03:44
MSSQLHtml :active, 03:44, 05:22
section Lane 3
ScopesJson :active, 00:00, 04:11
section Lane 4
MSSQLAuth :active, 00:00, 06:13
section Lane 5
SQLite3Live :active, 00:00, 01:48
UmbrellaPostgres :active, 01:48, 04:58
section Lane 6
UmbrellaPostgresAuth :crit, active, 00:00, 06:13
section Lane 7
MySQLHtml :active, 00:00, 01:56
SQLite3Auth :active, 01:56, 06:21
section Lane 8
MySQLScopes :active, 00:00, 02:07
UmbrellaPostgresHtml :active, 02:07, 05:29
section Lane 9
MSSQLJson :active, 00:00, 01:51
MySQLLive :active, 01:51, 03:42
MinimalApp :active, 03:42, 05:53
section Lane 10
UmbrellaPostgresJson :active, 00:00, 03:45
SQLite3Html :active, 03:45, 05:23
section Lane 11
ScopesCustomRoutes :active, 00:00, 02:58
ScopesLive :active, 02:58, 05:07
section Lane 12
MySQLAuth :active, 00:00, 06:12
section Lane 13
ScopesHtml :active, 00:00, 02:09
MySQLJson :active, 02:09, 04:03
section Lane 14
MSSQLLive :active, 00:00, 01:56
SQLite3Json :active, 01:56, 03:45
section Lane 15
PostgresAuth :active, 00:00, 05:55
section Lane 16
Postgres :active, 00:00, 03:24
UmbrellaPostgresLive :active, 03:24, 05:55
I'll drop the |
Before (main@b4056c3)https://github.com/phoenixframework/phoenix/actions/runs/33510641219/attempts/1#summary-99865178194 Phoenix Integration Tests (Elixir 1.18.4 / OTP 27): Passed
Module Execution Timelinegantt
title Module Execution Timeline
dateFormat mm:ss
axisFormat %M:%S
section Modules
UmbrellaAppWithDefaultsTest :crit, active, 00:00, 07:47
AppWithDefaultsTest :active, 00:00, 07:47
AppWithMySqlAdapterTest :active, 00:00, 07:03
AppWithScopesTest :active, 00:00, 06:22
AppWithMSSQLAdapterTest :active, 00:00, 06:04
AppWithSQLite3AdapterTest :active, 00:00, 05:54
AppWithNoOptionsTest :active, 00:00, 01:23
Phoenix Integration Tests (Elixir 1.20.4 / OTP 29): Passed
Module Execution Timelinegantt
title Module Execution Timeline
dateFormat mm:ss
axisFormat %M:%S
section Modules
UmbrellaAppWithDefaultsTest :crit, active, 00:00, 07:31
AppWithDefaultsTest :active, 00:00, 07:12
AppWithMySqlAdapterTest :active, 00:00, 06:38
AppWithMSSQLAdapterTest :active, 00:00, 06:01
AppWithScopesTest :active, 00:00, 05:59
AppWithSQLite3AdapterTest :active, 00:00, 05:56
AppWithNoOptionsTest :active, 00:00, 01:21
After (this PR)https://github.com/phoenixframework/phoenix/actions/runs/33567436374/attempts/1#summary-100053715601 Phoenix Integration Tests (Elixir 1.18.4 / OTP 27): Passed
Module Execution Timeline---
displayMode: compact
---
gantt
title Module Execution Timeline
dateFormat mm:ss
axisFormat %M:%S
todayMarker off
section Lane 1
SQLite3Live :active, 00:00, 01:10
Postgres :active, 01:10, 02:34
ScopesCustomRoutes :active, 02:34, 04:36
MySQLLive :active, 04:36, 05:36
MSSQLAuth :active, 05:36, 07:48
section Lane 2
SQLite3Html :active, 00:00, 01:12
MySQLAuth :active, 01:12, 06:02
section Lane 3
MySQLJson :active, 00:00, 01:11
PostgresAuth :active, 01:11, 05:55
MinimalApp :active, 05:55, 07:12
section Lane 4
PostgresLive :active, 00:00, 02:16
ScopesLive :active, 02:16, 03:53
MySQLScopes :active, 03:53, 05:20
UmbrellaPostgresJson :active, 05:20, 06:51
section Lane 5
SQLite3Json :active, 00:00, 01:04
UmbrellaPostgresAuth :crit, active, 01:04, 05:57
section Lane 6
UmbrellaPostgresHtml :active, 00:00, 02:09
MSSQLJson :active, 02:09, 03:03
MySQLHtml :active, 03:03, 04:05
UmbrellaPostgres :active, 04:05, 05:54
PostgresJson :active, 05:54, 07:13
section Lane 7
UmbrellaPostgresLive :active, 00:00, 02:16
ScopesHtml :active, 02:16, 03:46
PostgresHtml :active, 03:46, 05:34
MSSQLHtml :active, 05:34, 06:40
section Lane 8
SQLite3Auth :active, 00:00, 04:22
MSSQLLive :active, 04:22, 05:23
ScopesJson :active, 05:23, 07:31
Phoenix Integration Tests (Elixir 1.20.4 / OTP 29): Passed
Module Execution Timeline---
displayMode: compact
---
gantt
title Module Execution Timeline
dateFormat mm:ss
axisFormat %M:%S
todayMarker off
section Lane 1
MySQLHtml :active, 00:00, 00:39
ScopesCustomRoutes :active, 00:39, 01:59
UmbrellaPostgresAuth :active, 01:59, 04:16
section Lane 2
MSSQLLive :active, 00:00, 00:47
ScopesJson :active, 00:47, 02:26
PostgresHtml :active, 02:26, 03:43
section Lane 3
UmbrellaPostgresHtml :active, 00:00, 01:05
MSSQLJson :active, 01:05, 01:46
UmbrellaPostgres :active, 01:46, 02:51
MySQLLive :active, 02:51, 03:23
SQLite3Html :active, 03:23, 03:59
section Lane 4
UmbrellaPostgresJson :active, 00:00, 01:14
MySQLScopes :active, 01:14, 02:18
SQLite3Live :active, 02:18, 02:58
SQLite3Json :active, 02:58, 03:35
MySQLJson :active, 03:35, 04:04
section Lane 5
ScopesLive :active, 00:00, 01:00
SQLite3Auth :crit, active, 01:00, 03:45
section Lane 6
UmbrellaPostgresLive :active, 00:00, 01:14
Postgres :active, 01:14, 02:18
PostgresAuth :active, 02:18, 04:27
section Lane 7
PostgresLive :active, 00:00, 01:14
MinimalApp :active, 01:14, 02:12
MySQLAuth :active, 02:12, 04:25
section Lane 8
PostgresJson :active, 00:00, 01:14
MSSQLHtml :active, 01:14, 01:57
ScopesHtml :active, 01:57, 02:57
MSSQLAuth :active, 02:57, 04:39
|
|
wow, newer Elixir has a noticeable performance improvement indeed with the parallel compilation. So it makes sense that more cases don't help if compiling is the bottleneck. Since the generated apps share almost all deps, I'm wondering if we should add a "warmup" run and then copy the _build folder into the other tests, such that they only need to compile the project itself, which should be much faster. |
Previously, integration tests were bundled into 7 monolithic test files.
Because ExUnit parallelizes test execution across *modules* while
executing tests *serially* within each module, this structure created
two major bottlenecks:
1. Low concurrency at startup: Only 7 worker processes could run at T=0,
underutilizing runners configured with higher concurrency (e.g. 8
workers in GitHub Actions).
2. Idle worker tail: Faster modules (such as SQLite or basic generators)
finished early, leaving most worker processes sitting completely idle
while the suite's wall-clock time was bounded by the slowest
monolithic modules (`AppWithScopesTest` and
`UmbrellaAppWithDefaultsTest`).
This splits the integration test suites into 33 focused, granular
modules all marked with `async: true`:
- Preserve all 53 tests.
- Individual test modules should run under 3m00s.
- Fix a missing `@tag database: :sqlite3` on `test "has a passing test suite (--no-live)"`.
- File/module names follow the pattern
`app_with_{postgres,mysql,mssql,sqlite3}_adapter{,_auth_{html,live},_html,_json,_live,_scopes}_test.exs`
(and umbrella counterparts).
- Postgres adapter test modules explicitly named for symmetry with the
other database adapters (instead of "Default").
- Short, systematic app names per module to guarantee test database
isolation when executing concurrently against PostgreSQL, MySQL, and
MSSQL while ensuring all generated code strictly satisfies `mix
format` line-length limits.
- Update documentation in `integration_test/README.md` with rationale.
Previously, the Mermaid execution chart in SummaryFormatter rendered every test module on its own row, creating dozens of vertically stacked rows that consumed significant space in GitHub Step Summaries. This update: - Implements greedy interval scheduling to pack non-overlapping module executions into virtual worker lanes (corresponding to concurrent slots). - Configures Mermaid `displayMode: compact` so tasks within each lane render on a single horizontal row. - Disables Mermaid's default `todayMarker` to prevent a misplaced real-time vertical line across the relative mm:ss timeline. - Shortens module names for the Gantt timeline (e.g. `PostgresAuth` instead of `AppWithPostgresAdapterAuthTest`) to prevent SVG label collisions in compact boxes while retaining full names in the tables. - Highlights the single slowest module with `:crit` to clearly surface the primary test bottleneck. - Uses deterministic sorting with module name tie-breakers and efficient head-prepending in lane accumulation.
ece9aed to
44af0f2
Compare
Do you mean this? phoenix/integration_test/test/support/code_generator_case.ex Lines 19 to 24 in b4056c3 A further improvement to that will be using |
|
As a hopefully final update to this PR, I've further split the I've also documented the testing strategy and intended follow ups in the PR description. |
Ah, I forgot that we already do this. So maybe it works fine already (and only compiles the app files themselves) :) |
Previously, integration tests were bundled into 7 monolithic test files. Because ExUnit parallelizes test execution across modules while executing tests serially within each module, this structure created two major bottlenecks:
AppWithScopesTestandUmbrellaAppWithDefaultsTest).This splits the integration test suites into 33 focused, granular modules all marked with
async: true:@tag database: :sqlite3ontest "has a passing test suite (--no-live)".app_with_{postgres,mysql,mssql,sqlite3}_adapter{,_auth_{html,live},_html,_json,_live,_scopes}_test.exs(and umbrella counterparts).mix formatline-length limits.integration_test/README.mdwith rationale.Testing
To gain confidence that the split is mechanical and adheres to the changes documented above, I've used an adhoc script to compare the body of each of the 53 tests between
mainand the PR branch.Details
Intended follow ups
These are a few of the ideas to reduce wall clock time for integration tests (and the overall CI checks) down from 9-10min to roughly 3-4min:
phx.newflags) following the pattern:Assisted by Antigravity / Gemini 3.7 Flash.