Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4624ae6461
|
||
|
|
d5288c55a6
|
||
|
|
8784d913e6
|
||
|
|
25a24f1bc8
|
||
|
|
f68d370e92
|
||
|
|
afe9b54831 | ||
|
|
3741e90059
|
||
|
|
257788ec60
|
||
|
|
40bc96ba7c
|
||
|
|
a80c090ad3
|
||
|
|
311de2c433
|
||
|
|
d79b413626
|
||
|
|
10fe5215b6
|
||
|
|
ebb2ba18fb | ||
|
|
7803c5577b | ||
|
|
f1d8916d9f | ||
|
|
d79ec6a105 | ||
|
|
fb64a3ecb0 | ||
|
|
a0aef77ae4 | ||
|
|
909652f1f2 | ||
|
|
2da172a50e | ||
|
|
6a05a82221
|
||
|
|
ec2e40ba41 | ||
|
|
9ca5f13874
|
No files matched your search
+11
-57
@@ -1,60 +1,14 @@
|
||||
version: 2.1
|
||||
|
||||
setup: true
|
||||
orbs:
|
||||
qqq-orb: kingsrook/qqq-orb@0.6.9
|
||||
|
||||
commands:
|
||||
configure-package-install:
|
||||
steps:
|
||||
- run:
|
||||
name: Keep package installation unattended
|
||||
command: |
|
||||
# These build machines are discarded after the job.
|
||||
sudo install -d /etc/needrestart/conf.d
|
||||
printf '%s\n' '$nrconf{restart} = "l";' | sudo tee /etc/needrestart/conf.d/99-qqq-ci.conf >/dev/null
|
||||
|
||||
qqq-orb: kingsrook/qqq-orb@0.8
|
||||
workflows:
|
||||
test_only:
|
||||
jobs:
|
||||
- qqq-orb/mvn_test_only:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
filters:
|
||||
branches:
|
||||
ignore: /(develop|release\/.*)/
|
||||
|
||||
publish_snapshot:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
branch_type: snapshot
|
||||
filters:
|
||||
branches:
|
||||
only: [develop]
|
||||
|
||||
publish_release_candidate:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
branch_type: release_candidate
|
||||
filters:
|
||||
branches:
|
||||
only: [/release\/.*/]
|
||||
|
||||
publish_release:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
branch_type: release
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*/
|
||||
tags:
|
||||
only: [/^v\d+\.\d+\.\d+$/]
|
||||
setup:
|
||||
jobs:
|
||||
- qqq-orb/generate_pipeline:
|
||||
orb_version: '0.8'
|
||||
filters:
|
||||
branches:
|
||||
ignore: automation/qbit-rc-pending
|
||||
tags:
|
||||
only: /^v\d+\.\d+\.\d+$/
|
||||
@@ -0,0 +1,7 @@
|
||||
name: build
|
||||
on:
|
||||
push:
|
||||
jobs:
|
||||
maven:
|
||||
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1
|
||||
secrets: inherit
|
||||
@@ -0,0 +1,23 @@
|
||||
name: CodeQL
|
||||
'on':
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
- release/**
|
||||
- hotfix/**
|
||||
pull_request: {}
|
||||
schedule:
|
||||
- cron: 41 16 * * 0
|
||||
workflow_dispatch: {}
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
security-events: write
|
||||
packages: read
|
||||
concurrency:
|
||||
group: codeql-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
jobs:
|
||||
analyze:
|
||||
uses: QRun-IO/qqq-orb/.github/workflows/qbit-codeql.yml@c288a8864fe9054f68410e1d78f5863aea4c4d22
|
||||
@@ -1,26 +1,74 @@
|
||||
# Cut the next release candidate (QRun-IO/qqq docs/QRUN-QBIT-REPO-STANDARD.md, C6):
|
||||
# run this workflow by hand, or merge a develop PR labelled `release:rc`. It merges develop into
|
||||
# the release line and the release/* push runs CircleCI publish_release_candidate.
|
||||
name: Release candidate
|
||||
|
||||
on:
|
||||
'on':
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: Print the plan without merging or creating branches
|
||||
description: Plan a cut without changing release state
|
||||
type: boolean
|
||||
default: false
|
||||
finalize:
|
||||
description: Retry finalization of an existing cut; do not cut another RC
|
||||
type: boolean
|
||||
default: false
|
||||
recover:
|
||||
description: Recover the same published RC after a deployment timeout; never
|
||||
reupload
|
||||
type: boolean
|
||||
default: false
|
||||
circle_build:
|
||||
description: Original Circle publisher build number (recovery only)
|
||||
type: string
|
||||
default: ''
|
||||
merge_sha:
|
||||
description: Existing cut merge_sha (finalize retry only)
|
||||
type: string
|
||||
default: ''
|
||||
bump_sha:
|
||||
description: Existing cut bump_sha (finalize retry only)
|
||||
type: string
|
||||
default: ''
|
||||
branch:
|
||||
description: Existing cut branch (finalize retry only)
|
||||
type: string
|
||||
default: ''
|
||||
version:
|
||||
description: Existing cut version (finalize retry only)
|
||||
type: string
|
||||
default: ''
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches: [develop]
|
||||
|
||||
types:
|
||||
- closed
|
||||
branches:
|
||||
- develop
|
||||
repository_dispatch:
|
||||
types:
|
||||
- qbit-rc-published
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
actions: read
|
||||
checks: read
|
||||
jobs:
|
||||
release-rc:
|
||||
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release:rc'))
|
||||
uses: QRun-IO/.github/.github/workflows/qbit-release-rc.yml@main
|
||||
secrets: inherit
|
||||
if: github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch'
|
||||
|| (github.event_name == 'pull_request' && github.event.pull_request.merged
|
||||
== true && github.event.pull_request.base.ref == 'develop' && contains(github.event.pull_request.labels.*.name,
|
||||
'release:rc'))
|
||||
uses: QRun-IO/qqq-orb/.github/workflows/qbit-release-rc.yml@cc30f599e63c8e41629c99d23395bac1adb8bc44
|
||||
with:
|
||||
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
|
||||
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false
|
||||
}}
|
||||
finalize: ${{ github.event_name == 'repository_dispatch' || (github.event_name
|
||||
== 'workflow_dispatch' && inputs.finalize) }}
|
||||
recover: ${{ github.event_name == 'workflow_dispatch' && inputs.recover || false
|
||||
}}
|
||||
circle_build: ${{ github.event_name == 'workflow_dispatch' && inputs.circle_build
|
||||
|| '' }}
|
||||
merge_sha: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.merge_sha
|
||||
|| inputs.merge_sha || '' }}
|
||||
bump_sha: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.bump_sha
|
||||
|| inputs.bump_sha || '' }}
|
||||
branch: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch
|
||||
|| inputs.branch || '' }}
|
||||
version: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.version
|
||||
|| inputs.version || '' }}
|
||||
secrets: inherit
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
profile: qbit
|
||||
orb_version: '0.8'
|
||||
default_branch: develop
|
||||
java_version: '21'
|
||||
browsers: true
|
||||
api_checks: false
|
||||
docs_only: false
|
||||
release_on_main: false
|
||||
release_tag_policy: stable
|
||||
test_branch_policy: non-release
|
||||
publish:
|
||||
snapshot: true
|
||||
release_candidate: true
|
||||
release: true
|
||||
asciidoc: false
|
||||
release_completion: true
|
||||
@@ -72,6 +72,10 @@ Tier, Scope, Source, Strategy, Metric, Comparator and Severity use dropdowns wit
|
||||
|
||||
The changelog's changeset 10 seeds 13 reference budgets (Google's "good" thresholds and the Lighthouse category bands); nothing else is seeded. The target table is filled by Sync pages from sitemap, and the lab, finding, report, field, regression, change and comparison tables are filled by the qbit as it runs (empty is a normal start). Every table's help (`help/qbit-web-performance.yaml`, shown on the table screen) says how its rows get created, and the Setup card on the home shows what still needs doing. Budgets with source RUM are not evaluated yet (no real-user source is wired); LAB and CRUX budgets are.
|
||||
|
||||
## Scheduled audit quota
|
||||
|
||||
Set `withMaxRunsPerDay(300)` in the host configuration to reserve headroom below the default 500-run cap. Key-page schedules, full audits and manually queued runs share that daily allowance in the configured time zone. Scheduled requests prioritize page/device pairs never queued before, then those queued least recently. Quota-skipped rows do not advance that rotation, so large sitemaps continue through the remaining pairs on later days instead of repeatedly testing the first pages. Existing queued/running pairs still merge; manual selection keeps its requested order.
|
||||
|
||||
## In-app help
|
||||
|
||||
The QBit registers [`help/qbit-web-performance.yaml`](src/main/resources/help/qbit-web-performance.yaml) with QQQ help bundles (RC.7 or later). Registration is metadata-only; core skips startup synchronization when the host has no `helpContent` table. App and table empty-state help remains available without Help installed.
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<properties>
|
||||
<revision>0.1.0-RC.6</revision>
|
||||
<qqq.version>4.1.0-RC.26</qqq.version> <!-- feeds qbit-build-parent's qqq-bom-pom import -->
|
||||
<revision>0.1.0-SNAPSHOT</revision>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<jacoco.version>0.8.15</jacoco.version>
|
||||
@@ -29,7 +30,7 @@
|
||||
<parent>
|
||||
<groupId>com.kingsrook</groupId>
|
||||
<artifactId>qbit-build-parent</artifactId>
|
||||
<version>2.0.0</version>
|
||||
<version>2.2.1</version>
|
||||
</parent>
|
||||
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
@@ -212,7 +213,7 @@
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<properties>
|
||||
<qqq.snapshot.version>4.1.0-RC.7</qqq.snapshot.version>
|
||||
<qqq.snapshot.version>4.1.0-RC.26</qqq.snapshot.version>
|
||||
</properties>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
|
||||
@@ -209,7 +209,8 @@ public final class ScheduledSteps
|
||||
try
|
||||
{
|
||||
WebPerfService service = WebPerfRuntime.forInput(input).service();
|
||||
WebPerfService.QueuePlan plan = service.enqueue(service.requestsFor(service.enabledTargets(keyOnly), "SCHEDULE", reason, Instant.now()), Instant.now());
|
||||
Instant now = Instant.now();
|
||||
WebPerfService.QueuePlan plan = service.enqueue(service.scheduledRequests(keyOnly, reason, now), now);
|
||||
LOG.info("Web performance tests queued", logPair("reason", reason), logPair("queued", plan.queue().size()), logPair("merged", plan.merged().size()),
|
||||
logPair("overCap", plan.overQuota().size()));
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -448,6 +450,32 @@ public class WebPerfService
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Scheduled requests, least recently queued page/strategy first. Quota skips
|
||||
** do not advance the rotation; never-tested pairs precede historical runs.
|
||||
** Manual requests keep the caller's ordering.
|
||||
**
|
||||
** @param keyOnly only KEY pages
|
||||
** @param reason schedule description
|
||||
** @param now earliest start
|
||||
** @return requests ordered fairly across daily quota boundaries
|
||||
** @throws SQLException on a history read failure
|
||||
*******************************************************************************/
|
||||
public List<RunRequest> scheduledRequests(boolean keyOnly, String reason, Instant now) throws SQLException
|
||||
{
|
||||
List<RunRequest> requests = requestsFor(enabledTargets(keyOnly), "SCHEDULE", reason, now);
|
||||
Map<String, Instant> lastQueued = new HashMap<>();
|
||||
for(Map<String, Object> row : store.query("SELECT r.target_id, r.strategy, MAX(r.queued_at) AS last_queued FROM {lab_run} r JOIN {target} t ON t.id = r.target_id WHERE t.site_key = ? AND r.status <> 'SKIPPED_QUOTA' GROUP BY r.target_id, r.strategy", siteKey()))
|
||||
{
|
||||
lastQueued.put(WebPerfStore.asLong(row.get("target_id")) + "|" + row.get("strategy"), (Instant) row.get("last_queued"));
|
||||
}
|
||||
requests.sort(Comparator.comparing(request -> lastQueued.get(request.targetId() + "|" + request.strategy()),
|
||||
Comparator.nullsFirst(Comparator.naturalOrder())));
|
||||
return (requests);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Enabled pages, optionally only the KEY tier.
|
||||
**
|
||||
|
||||
@@ -271,6 +271,73 @@ class WebPerfP1Test
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** An odd daily cap rotates individual device strategies, not just pages.
|
||||
** Skipped rows and repeated scheduling must not starve the remaining pairs.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void scheduledQuotaRotatesPagesAndStrategies() throws Exception
|
||||
{
|
||||
config.withMaxRunsPerDay(3);
|
||||
Instant dayOne = Instant.parse("2026-10-19T12:00:00Z");
|
||||
WebPerfService service = service();
|
||||
service.applySync(service.planSync(sitemap), dayOne);
|
||||
WebPerfService.QueuePlan first = service.enqueue(service.scheduledRequests(false, "audit", dayOne), dayOne);
|
||||
assertEquals(3, first.queue().size());
|
||||
assertEquals(3, first.overQuota().size());
|
||||
assertEquals(0, service.enqueue(service.scheduledRequests(false, "audit", dayOne), dayOne).queue().size());
|
||||
store().update("UPDATE {lab_run} SET status = 'SUCCEEDED' WHERE status = 'QUEUED'");
|
||||
|
||||
Instant dayTwo = dayOne.plusSeconds(86400);
|
||||
WebPerfService.QueuePlan second = service.enqueue(service.scheduledRequests(false, "audit", dayTwo), dayTwo);
|
||||
assertEquals(3, second.queue().size());
|
||||
List<String> firstPairs = first.queue().stream().map(run -> run.targetId() + "|" + run.strategy()).toList();
|
||||
assertThat(second.queue().stream().map(run -> run.targetId() + "|" + run.strategy()).toList())
|
||||
.doesNotContainAnyElementsOf(firstPairs);
|
||||
assertEquals(6, store().query("SELECT DISTINCT target_id, strategy FROM {lab_run} WHERE status <> 'SKIPPED_QUOTA'").size());
|
||||
assertThat(service.scheduledRequests(true, "key pages", dayTwo)).hasSize(2)
|
||||
.allSatisfy(run -> assertEquals(SITE + "/", run.url()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Manual and key-page runs consume the same allowance as the full audit.
|
||||
** Persisted history rotates the remaining pairs after a service restart.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void scheduledQuotaSharesAllowanceAndSurvivesRestart() throws Exception
|
||||
{
|
||||
config.withMaxRunsPerDay(3);
|
||||
Instant day = Instant.parse("2026-10-19T12:00:00Z");
|
||||
WebPerfService service = service();
|
||||
service.applySync(service.planSync(sitemap), day);
|
||||
List<WebPerfService.RunRequest> manual = service.requestsFor(service.enabledTargets(false), "MANUAL", "selected page", day);
|
||||
assertEquals(1, service.enqueue(List.of(manual.get(3)), day).queue().size());
|
||||
assertEquals(2, service.enqueue(service.scheduledRequests(true, "key pages", day), day).queue().size());
|
||||
assertEquals(0, service.enqueue(service.scheduledRequests(false, "audit", day), day).queue().size());
|
||||
store().update("UPDATE {lab_run} SET status = 'SUCCEEDED' WHERE status = 'QUEUED'");
|
||||
|
||||
List<String> rotated = new ArrayList<>();
|
||||
for(int nextDay = 1; nextDay <= 3; nextDay++)
|
||||
{
|
||||
Instant now = day.plusSeconds(86400L * nextDay);
|
||||
WebPerfService restarted = new WebPerfService(new WebPerfRuntime(config), store());
|
||||
assertEquals(2, restarted.enqueue(restarted.scheduledRequests(true, "key pages", now), now).queue().size());
|
||||
WebPerfService.QueuePlan audit = restarted.enqueue(restarted.scheduledRequests(false, "audit", now), now);
|
||||
assertEquals(1, audit.queue().size());
|
||||
assertEquals(2, audit.merged().size());
|
||||
WebPerfService.RunRequest queued = audit.queue().get(0);
|
||||
assertThat(queued.url()).isNotEqualTo(SITE + "/");
|
||||
rotated.add(queued.targetId() + "|" + queued.strategy());
|
||||
store().update("UPDATE {lab_run} SET status = 'SUCCEEDED' WHERE status = 'QUEUED'");
|
||||
}
|
||||
assertThat(rotated).doesNotHaveDuplicates();
|
||||
assertEquals(6, store().query("SELECT DISTINCT target_id, strategy FROM {lab_run} WHERE status <> 'SKIPPED_QUOTA'").size());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The Setup card reports pages, budgets, the lab runner and field data from
|
||||
** the runtime's own resolution, with the next step for each.
|
||||
|
||||
Reference in new issue
Block a user