Skip to content

Commit 857ddc4

Browse files
authored
Make Pipeline Overview responsive on large in-progress builds (#1225)
1 parent a457d5d commit 857ddc4

31 files changed

Lines changed: 1614 additions & 358 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,6 @@ src/main/webapp/js/bundles
2525

2626
# macOS
2727
.DS_Store
28+
29+
results.txt
30+
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Stress-test pipeline that produces roughly 300,000 FlowNodes across three
2+
// bulk parallel phases, with short sleep windows between them so you can poll
3+
// the REST API and watch latency as the graph grows.
4+
//
5+
// Give Jenkins at least 2 GB heap before running — each FlowNode retains a few
6+
// KB, and GC thrash will muddy any measurement you take from it.
7+
//
8+
// Knobs (multiplicative):
9+
// BRANCHES - parallel branches per phase (default 50)
10+
// STAGES_PER_SECTION - sequential stages inside each branch per phase (default 20)
11+
// STEPS_PER_STAGE - echo steps per stage (default 100)
12+
// Defaults yield 3 * 50 * 20 * 100 = 300,000 echo FlowNodes, plus ~6,000
13+
// stage/parallel wrapping nodes.
14+
//
15+
// Runtime is dominated by CPS step-evaluation (a few ms per echo), so the
16+
// defaults take 30–45 minutes on a typical controller. Drop STEPS_PER_STAGE
17+
// for faster iteration while keeping the structural shape.
18+
19+
BRANCHES = 50
20+
STAGES_PER_SECTION = 20
21+
STEPS_PER_STAGE = 100
22+
23+
def bulkSection(int branches, int stagesPerBranch, int stepsPerStage, String label) {
24+
def work = [:]
25+
for (int i = 0; i < branches; i++) {
26+
def idx = i
27+
work["${label}-b${idx}"] = {
28+
for (int j = 0; j < stagesPerBranch; j++) {
29+
stage("${label}-b${idx}-s${j}") {
30+
for (int k = 0; k < stepsPerStage; k++) {
31+
echo "."
32+
}
33+
}
34+
}
35+
}
36+
}
37+
parallel work
38+
}
39+
40+
node {
41+
stage('Warmup') {
42+
echo "BRANCHES=${BRANCHES} STAGES_PER_SECTION=${STAGES_PER_SECTION} STEPS_PER_STAGE=${STEPS_PER_STAGE}"
43+
echo "Expected echo nodes: ~${3 * BRANCHES * STAGES_PER_SECTION * STEPS_PER_STAGE}"
44+
}
45+
46+
stage('Settle (tiny graph)') { sleep 30 }
47+
stage('Bulk 1/3') { bulkSection(BRANCHES, STAGES_PER_SECTION, STEPS_PER_STAGE, 's1') }
48+
49+
stage('Settle (~100k nodes)') { sleep 30 }
50+
stage('Bulk 2/3') { bulkSection(BRANCHES, STAGES_PER_SECTION, STEPS_PER_STAGE, 's2') }
51+
52+
stage('Settle (~200k nodes)') { sleep 30 }
53+
stage('Bulk 3/3') { bulkSection(BRANCHES, STAGES_PER_SECTION, STEPS_PER_STAGE, 's3') }
54+
55+
stage('Settle (~300k nodes, still building)') { sleep 30 }
56+
stage('Settle (completed)') { sleep 60 }
57+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// A medium-sized pipeline exercising sequential stages, flat parallel, nested
2+
// parallel, and wide branch counts. Useful when you want to see the Pipeline
3+
// Overview render a non-trivial graph without waiting on a stress test.
4+
//
5+
// Shape: ~70 user-visible stages, a few hundred FlowNodes. Runs in a minute or
6+
// two. Tune the sleep values and branch counts for bigger or smaller graphs.
7+
8+
def runStage(String name, int sleepSeconds) {
9+
stage(name) {
10+
echo "enter: ${name}"
11+
sleep sleepSeconds
12+
writeFile file: "out/${name.replaceAll('[^A-Za-z0-9]', '_')}.txt", text: name
13+
echo "exit: ${name}"
14+
}
15+
}
16+
17+
node {
18+
runStage('Checkout', 2)
19+
runStage('Install dependencies', 3)
20+
runStage('Lint', 2)
21+
runStage('Format check', 2)
22+
runStage('Static analysis', 2)
23+
24+
stage('Build matrix') {
25+
def matrix = [:]
26+
['linux', 'macos', 'windows', 'freebsd', 'illumos', 'aix'].each { os ->
27+
matrix["Build ${os}"] = {
28+
runStage("${os}: fetch", 1)
29+
runStage("${os}: compile", 3)
30+
runStage("${os}: unit tests", 2)
31+
runStage("${os}: package", 1)
32+
}
33+
}
34+
parallel matrix
35+
}
36+
37+
stage('Integration') {
38+
parallel(
39+
'api': {
40+
parallel(
41+
'api: smoke': { runStage('api smoke', 2) },
42+
'api: contract': { runStage('api contract', 3) },
43+
'api: regression': { runStage('api regression', 4) },
44+
)
45+
},
46+
'browser': {
47+
parallel(
48+
'browser: chrome': { runStage('chrome', 3) },
49+
'browser: firefox': { runStage('firefox', 3) },
50+
'browser: safari': { runStage('safari', 3) },
51+
'browser: edge': { runStage('edge', 3) },
52+
)
53+
},
54+
'perf': {
55+
runStage('perf: baseline', 2)
56+
runStage('perf: load', 4)
57+
runStage('perf: stress', 5)
58+
},
59+
'security': {
60+
runStage('dep audit', 2)
61+
runStage('sast', 3)
62+
runStage('dast', 4)
63+
},
64+
)
65+
}
66+
67+
stage('Publish artifacts') {
68+
def publishers = [:]
69+
// C-style loop: (1..8).each would push an IntRange through a parallel
70+
// closure, which is not CPS-serialisable and fails at checkpoint.
71+
for (int i = 1; i <= 8; i++) {
72+
def idx = i
73+
publishers["Publish region ${idx}"] = {
74+
runStage("upload region ${idx}", 1)
75+
runStage("verify region ${idx}", 1)
76+
}
77+
}
78+
parallel publishers
79+
}
80+
81+
runStage('Release notes', 1)
82+
runStage('Changelog', 2)
83+
runStage('Tag', 2)
84+
runStage('Announce', 1)
85+
runStage('Cleanup', 1)
86+
}

docs/examples/perf-observer.sh

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
#!/usr/bin/env bash
2+
#
3+
# perf-observer.sh — polls /tree and /allSteps for a running build and prints a
4+
# latency summary at the end. Use it to compare two configurations (e.g. before
5+
# vs. after a change) by running it once per build and diffing the two summaries.
6+
#
7+
# Usage:
8+
# ./perf-observer.sh JENKINS_URL JOB_NAME BUILD_NUMBER [LABEL]
9+
#
10+
# Optional env vars:
11+
# JENKINS_AUTH=user:token pass-through to curl --user
12+
# POLL_INTERVAL=3 seconds between poll rounds (default 3)
13+
#
14+
# Example:
15+
#
16+
# ./perf-observer.sh http://localhost:8080/jenkins perf 1 baseline
17+
# ./perf-observer.sh http://localhost:8080/jenkins perf 2 after-change
18+
#
19+
# Raw samples are written to /tmp/pgv-perf-<LABEL>.csv for later analysis.
20+
21+
set -euo pipefail
22+
23+
if [[ $# -lt 3 ]]; then
24+
echo "Usage: $0 JENKINS_URL JOB_NAME BUILD_NUMBER [LABEL]" >&2
25+
exit 2
26+
fi
27+
28+
JENKINS_URL="${1%/}"
29+
JOB_NAME="$2"
30+
BUILD_NUMBER="$3"
31+
LABEL="${4:-run}"
32+
POLL_INTERVAL="${POLL_INTERVAL:-3}"
33+
CSV="/tmp/pgv-perf-${LABEL}.csv"
34+
35+
BASE="${JENKINS_URL}/job/${JOB_NAME}/${BUILD_NUMBER}/stages"
36+
API_URL="${JENKINS_URL}/job/${JOB_NAME}/${BUILD_NUMBER}/api/json?tree=result,inProgress"
37+
38+
AUTH=()
39+
if [[ -n "${JENKINS_AUTH:-}" ]]; then
40+
AUTH=(--user "$JENKINS_AUTH")
41+
fi
42+
43+
echo "Observing: $BASE"
44+
echo "Label: $LABEL"
45+
echo "CSV: $CSV"
46+
echo "Interval: ${POLL_INTERVAL}s"
47+
echo
48+
49+
echo "sample_ts,endpoint,http_code,time_ms,bytes" > "$CSV"
50+
51+
measure() {
52+
local endpoint="$1"
53+
local url="$BASE/$endpoint"
54+
local out
55+
out=$(curl -so /dev/null --compressed -w '%{http_code},%{time_total},%{size_download}' \
56+
"${AUTH[@]}" "$url" 2>/dev/null || echo "000,0,0")
57+
local http_code="${out%%,*}"
58+
local rest="${out#*,}"
59+
local time_s="${rest%%,*}"
60+
local bytes="${rest#*,}"
61+
local time_ms
62+
time_ms=$(awk -v t="$time_s" 'BEGIN { printf "%.1f", t * 1000 }')
63+
local ts
64+
ts=$(date +%s)
65+
echo "$ts,$endpoint,$http_code,$time_ms,$bytes" >> "$CSV"
66+
printf ' %s %-10s %s %8sms %8s bytes\n' \
67+
"$(date +%H:%M:%S)" "$endpoint" "$http_code" "$time_ms" "$bytes"
68+
}
69+
70+
build_state() {
71+
curl -s "${AUTH[@]}" "$API_URL" 2>/dev/null || echo ""
72+
}
73+
74+
is_running() {
75+
local resp
76+
resp=$(build_state)
77+
[[ "$resp" == *'"inProgress":true'* ]]
78+
}
79+
80+
echo "Waiting for build to enter in-progress state..."
81+
for _ in $(seq 1 30); do
82+
if is_running; then break; fi
83+
sleep 2
84+
done
85+
86+
if ! is_running; then
87+
state=$(build_state)
88+
if [[ -z "$state" ]]; then
89+
echo "ERROR: could not reach $API_URL" >&2
90+
exit 1
91+
fi
92+
echo "NOTE: build is not in-progress (state: $state). Taking a few post-completion samples only."
93+
fi
94+
95+
echo
96+
echo "Polling while build runs..."
97+
echo
98+
99+
while is_running; do
100+
measure tree
101+
measure allSteps
102+
sleep "$POLL_INTERVAL"
103+
done
104+
105+
echo
106+
echo "Build complete. Taking three post-completion samples..."
107+
for _ in 1 2 3; do
108+
measure tree
109+
measure allSteps
110+
sleep 1
111+
done
112+
113+
echo
114+
echo "================ Summary: '$LABEL' ================"
115+
python3 - "$CSV" <<'PY'
116+
import csv, sys
117+
118+
csv_path = sys.argv[1]
119+
samples = {}
120+
warnings = {}
121+
122+
with open(csv_path) as f:
123+
for row in csv.DictReader(f):
124+
ep = row["endpoint"]
125+
code = row["http_code"]
126+
try:
127+
t = float(row["time_ms"])
128+
except ValueError:
129+
continue
130+
if code != "200":
131+
warnings[(ep, code)] = warnings.get((ep, code), 0) + 1
132+
continue
133+
samples.setdefault(ep, []).append(t)
134+
135+
def pct(sorted_vals, p):
136+
if not sorted_vals:
137+
return 0.0
138+
idx = int(round((p / 100.0) * (len(sorted_vals) - 1)))
139+
idx = max(0, min(len(sorted_vals) - 1, idx))
140+
return sorted_vals[idx]
141+
142+
for ep in sorted(samples):
143+
vals = sorted(samples[ep])
144+
n = len(vals)
145+
print(f" {ep:10s} n={n:4d} "
146+
f"min={vals[0]:8.1f}ms "
147+
f"median={pct(vals, 50):8.1f}ms "
148+
f"p95={pct(vals, 95):8.1f}ms "
149+
f"max={vals[-1]:8.1f}ms")
150+
151+
if warnings:
152+
print()
153+
for (ep, code), count in sorted(warnings.items()):
154+
print(f" {ep:10s} WARN {count}x non-200 ({code})")
155+
PY
156+
echo "====================================================="
157+
echo
158+
echo "Raw samples: $CSV"

pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@
6363
<groupId>io.jenkins.plugins</groupId>
6464
<artifactId>ionicons-api</artifactId>
6565
</dependency>
66+
<dependency>
67+
<groupId>io.jenkins.plugins</groupId>
68+
<artifactId>jackson3-api</artifactId>
69+
</dependency>
6670
<dependency>
6771
<groupId>org.jenkins-ci.plugins</groupId>
6872
<artifactId>display-url-api</artifactId>

0 commit comments

Comments
 (0)