Skip to content

Commit efc4f2f

Browse files
Reduce pull request performance checks (#99)
* Reduce pull request performance checks * Ignore non-package example directories * Forward the selected example matrix
1 parent 353700a commit efc4f2f

26 files changed

Lines changed: 559 additions & 1174 deletions

.changeset/calm-ravens-check.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@typeonce/effect-machine": patch
3+
---
4+
5+
Reduce pull request performance-check latency while preserving focused type,
6+
runtime, and memory regression coverage.

.github/pull_request_template.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111

1212
- [ ] `pnpm check`
1313
- [ ] Relevant example checks, when examples changed
14-
- [ ] Reviewed the automated type-performance report, when the public TypeScript API or inference changed
15-
- [ ] Reviewed the automated runtime-performance report, when runtime behavior changed
14+
- [ ] Automated type-performance measurement passed or was not required
15+
- [ ] Automated runtime- and memory-performance measurement passed or was not required
1616

1717
<!--
18-
The type- and runtime-performance workflows post and update their base-versus-PR comparisons automatically.
18+
The type- and runtime-performance workflows measure only relevant changes and post their base-versus-PR comparisons automatically.
1919
Do not copy a manually measured table into this description.
2020
-->

.github/workflows/ci.yml

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ name: CI
22

33
on:
44
pull_request:
5-
push:
6-
branches: [main]
75

86
concurrency:
97
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -49,11 +47,18 @@ jobs:
4947
discover-examples:
5048
runs-on: ubuntu-latest
5149
outputs:
52-
matrix: ${{ steps.examples.outputs.matrix }}
50+
matrix: ${{ steps.examples.outputs.examples }}
51+
required: ${{ steps.examples.outputs.examples_required }}
5352
steps:
5453
- uses: actions/checkout@v7
54+
with:
55+
fetch-depth: 0
5556
- id: examples
56-
run: echo "matrix=$(node scripts/list-examples.mjs)" >> "$GITHUB_OUTPUT"
57+
run: >-
58+
node scripts/ci-changes.mjs
59+
--base ${{ github.event.pull_request.base.sha }}
60+
--head ${{ github.event.pull_request.head.sha }}
61+
--github-output "$GITHUB_OUTPUT"
5762
5863
example:
5964
needs: discover-examples
@@ -92,7 +97,13 @@ jobs:
9297
- name: Require every example check to pass
9398
env:
9499
DISCOVERY_RESULT: ${{ needs.discover-examples.result }}
100+
EXAMPLES_REQUIRED: ${{ needs.discover-examples.outputs.required }}
95101
EXAMPLE_RESULT: ${{ needs.example.result }}
96102
run: |
97103
test "$DISCOVERY_RESULT" = "success"
98-
test "$EXAMPLE_RESULT" = "success"
104+
if test "$EXAMPLES_REQUIRED" = "true"; then
105+
test "$EXAMPLE_RESULT" = "success"
106+
else
107+
test "$EXAMPLE_RESULT" = "skipped"
108+
echo "Examples are not affected by this pull request." >> "$GITHUB_STEP_SUMMARY"
109+
fi

.github/workflows/release.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ name: Release
33
on:
44
push:
55
branches: [main]
6+
paths:
7+
- ".changeset/**"
8+
- "package.json"
9+
- "pnpm-lock.yaml"
10+
- "src/**"
611

712
concurrency:
813
group: release-${{ github.ref }}

.github/workflows/runtime-performance-comment.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ jobs:
1919
comment:
2020
if: >-
2121
github.event.workflow_run.event == 'pull_request' &&
22-
github.event.workflow_run.conclusion == 'success'
22+
github.event.workflow_run.conclusion != 'cancelled'
2323
runs-on: ubuntu-latest
2424
steps:
2525
- name: Check out trusted reporting code
2626
uses: actions/checkout@v7
2727

2828
- name: Download performance report
29+
id: download
30+
continue-on-error: true
2931
uses: actions/download-artifact@v8
3032
with:
3133
name: runtime-performance-report
@@ -34,13 +36,15 @@ jobs:
3436
run-id: ${{ github.event.workflow_run.id }}
3537

3638
- name: Render report from validated benchmark data
39+
if: steps.download.outcome == 'success'
3740
run: >-
3841
node scripts/compare-runtime-performance.mjs
3942
report/base
4043
report/head
4144
> "$RUNNER_TEMP/report.md"
4245
4346
- name: Create or update pull request comment
47+
if: steps.download.outcome == 'success'
4448
uses: actions/github-script@v9
4549
env:
4650
REPORT_PATH: ${{ runner.temp }}/report.md

.github/workflows/runtime-performance.yml

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,25 @@ permissions:
1111
contents: read
1212

1313
jobs:
14-
runtime-performance:
15-
name: runtime-performance
14+
changes:
15+
runs-on: ubuntu-latest
16+
outputs:
17+
required: ${{ steps.classify.outputs.runtime_performance }}
18+
steps:
19+
- uses: actions/checkout@v7
20+
with:
21+
fetch-depth: 0
22+
- id: classify
23+
run: >-
24+
node scripts/ci-changes.mjs
25+
--base ${{ github.event.pull_request.base.sha }}
26+
--head ${{ github.event.pull_request.head.sha }}
27+
--github-output "$GITHUB_OUTPUT"
28+
29+
measure:
30+
needs: changes
31+
if: needs.changes.outputs.required == 'true'
32+
name: runtime-performance measurement
1633
runs-on: ubuntu-latest
1734
timeout-minutes: 25
1835
steps:
@@ -52,6 +69,7 @@ jobs:
5269
pnpm --dir base build
5370
5471
- name: Measure base and pull request
72+
id: benchmark
5573
shell: bash
5674
run: |
5775
set -euo pipefail
@@ -79,16 +97,41 @@ jobs:
7997
measure base "$reports/base/5.json"
8098
measure head "$reports/head/5.json"
8199
82-
node head/scripts/compare-runtime-performance.mjs \
83-
"$reports/base" \
84-
"$reports/head" \
85-
> "$RUNNER_TEMP/runtime-performance-report.md"
100+
if ! node head/scripts/compare-runtime-performance.mjs \
101+
--check \
102+
"$reports/base" \
103+
"$reports/head" \
104+
> "$RUNNER_TEMP/runtime-performance-report.md"; then
105+
cat "$RUNNER_TEMP/runtime-performance-report.md" >> "$GITHUB_STEP_SUMMARY"
106+
exit 1
107+
fi
86108
cat "$RUNNER_TEMP/runtime-performance-report.md" >> "$GITHUB_STEP_SUMMARY"
87109
88110
- name: Upload report for the comment workflow
111+
if: always() && steps.benchmark.outcome != 'skipped'
89112
uses: actions/upload-artifact@v7
90113
with:
91114
name: runtime-performance-report
92115
path: ${{ runner.temp }}/runtime-performance
93116
if-no-files-found: error
94117
retention-days: 7
118+
119+
runtime-performance:
120+
if: always()
121+
needs: [changes, measure]
122+
name: runtime-performance
123+
runs-on: ubuntu-latest
124+
steps:
125+
- name: Require relevant runtime performance to pass
126+
env:
127+
CHANGE_RESULT: ${{ needs.changes.result }}
128+
MEASUREMENT_REQUIRED: ${{ needs.changes.outputs.required }}
129+
MEASUREMENT_RESULT: ${{ needs.measure.result }}
130+
run: |
131+
test "$CHANGE_RESULT" = "success"
132+
if test "$MEASUREMENT_REQUIRED" = "true"; then
133+
test "$MEASUREMENT_RESULT" = "success"
134+
else
135+
test "$MEASUREMENT_RESULT" = "skipped"
136+
echo "Runtime performance is not affected by this pull request." >> "$GITHUB_STEP_SUMMARY"
137+
fi

.github/workflows/type-performance-comment.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ jobs:
2626
uses: actions/checkout@v7
2727

2828
- name: Download performance report
29+
id: download
30+
continue-on-error: true
2931
uses: actions/download-artifact@v8
3032
with:
3133
name: type-performance-report
@@ -34,13 +36,15 @@ jobs:
3436
run-id: ${{ github.event.workflow_run.id }}
3537

3638
- name: Render report from validated benchmark data
39+
if: steps.download.outcome == 'success'
3740
run: >-
3841
node scripts/compare-type-performance.mjs
3942
report/before.json
4043
report/after.json
4144
> "$RUNNER_TEMP/report.md"
4245
4346
- name: Create or update pull request comment
47+
if: steps.download.outcome == 'success'
4448
uses: actions/github-script@v9
4549
env:
4650
REPORT_PATH: ${{ runner.temp }}/report.md

.github/workflows/type-performance.yml

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,25 @@ permissions:
1111
contents: read
1212

1313
jobs:
14-
type-performance:
15-
name: type-performance
14+
changes:
15+
runs-on: ubuntu-latest
16+
outputs:
17+
required: ${{ steps.classify.outputs.type_performance }}
18+
steps:
19+
- uses: actions/checkout@v7
20+
with:
21+
fetch-depth: 0
22+
- id: classify
23+
run: >-
24+
node scripts/ci-changes.mjs
25+
--base ${{ github.event.pull_request.base.sha }}
26+
--head ${{ github.event.pull_request.head.sha }}
27+
--github-output "$GITHUB_OUTPUT"
28+
29+
measure:
30+
needs: changes
31+
if: needs.changes.outputs.required == 'true'
32+
name: type-performance measurement
1633
runs-on: ubuntu-latest
1734
steps:
1835
- name: Check out base
@@ -66,3 +83,23 @@ jobs:
6683
${{ runner.temp }}/after.json
6784
if-no-files-found: error
6885
retention-days: 7
86+
87+
type-performance:
88+
if: always()
89+
needs: [changes, measure]
90+
name: type-performance
91+
runs-on: ubuntu-latest
92+
steps:
93+
- name: Require relevant type performance to pass
94+
env:
95+
CHANGE_RESULT: ${{ needs.changes.result }}
96+
MEASUREMENT_REQUIRED: ${{ needs.changes.outputs.required }}
97+
MEASUREMENT_RESULT: ${{ needs.measure.result }}
98+
run: |
99+
test "$CHANGE_RESULT" = "success"
100+
if test "$MEASUREMENT_REQUIRED" = "true"; then
101+
test "$MEASUREMENT_RESULT" = "success"
102+
else
103+
test "$MEASUREMENT_RESULT" = "skipped"
104+
echo "Type performance is not affected by this pull request." >> "$GITHUB_STEP_SUMMARY"
105+
fi

package.json

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"test": "vitest run",
5050
"test:types": "tstyche",
5151
"check:architecture": "node --test scripts/check-architecture.test.mjs && node scripts/check-architecture.mjs",
52+
"check:ci": "node --test scripts/ci-changes.test.mjs scripts/runtime-performance-regression.test.mjs",
5253
"typecheck": "tsc -p tsconfig.json --noEmit",
5354
"perf:types": "pnpm build && node scripts/type-performance.mjs",
5455
"perf:runtime": "pnpm build && node --expose-gc scripts/runtime-performance.mjs",
@@ -63,7 +64,7 @@
6364
"docs:site:serve": "node scripts/api-reference-site/serve.mjs",
6465
"test:consumer": "node scripts/test-consumer.mjs",
6566
"pack:check": "node scripts/pack-check.mjs",
66-
"check": "pnpm format:check && pnpm check:architecture && pnpm docs:api:check && pnpm docs:site:check && pnpm typecheck && pnpm build && pnpm test && pnpm test:types && pnpm test:consumer && pnpm pack:check",
67+
"check": "pnpm format:check && pnpm check:architecture && pnpm check:ci && pnpm docs:api:check && pnpm docs:site:check && pnpm typecheck && pnpm build && pnpm test && pnpm test:types && pnpm test:consumer && pnpm pack:check",
6768
"changeset": "changeset",
6869
"version-packages": "changeset version",
6970
"release": "pnpm build && changeset publish"
@@ -82,9 +83,7 @@
8283
"tstyche": "7.2.1",
8384
"typedoc": "0.28.20",
8485
"typescript": "6.0.3",
85-
"vitest": "4.1.10",
86-
"xstate-v5": "npm:xstate@5.32.5",
87-
"xstate-v6": "npm:xstate@6.0.0-alpha.36"
86+
"vitest": "4.1.10"
8887
},
8988
"packageManager": "pnpm@10.17.1",
9089
"engines": {

perf/runtime/README.md

Lines changed: 18 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# Runtime performance
22

3-
Run the local runtime benchmark suite against the compiled package and the
4-
pinned XState comparison versions:
3+
Run the local runtime benchmark suite against the compiled package:
54

65
```sh
76
pnpm perf:runtime
@@ -15,64 +14,33 @@ The command reports:
1514
- repeated child lookup and delivery to one running child;
1615
- machine start-and-stop throughput;
1716
- parent-with-child start-and-stop throughput;
18-
- Effect-only lifecycle reference points for a suspended fiber, a queue worker,
19-
a minimal actor shell, and a two-shell family;
20-
- Effect-only coordination reference points for an owner-only mutable snapshot,
21-
a synchronized snapshot, and a terminal `Deferred` latch;
17+
- generic and compiled raw-process lifecycle throughput;
2218
- heap and resident-memory growth at 100, 500, and 1,000 live units, including
2319
a raw generic process, a raw compiled process, an idle statechart, two
2420
independent statecharts, a parent with one child, that relationship with
2521
child-registry observation active, and an invoked child whose active snapshots
26-
are observed;
27-
- lower-bound memory profiles for Effect itself: a suspended fiber, a queue
28-
with a waiting fiber, a minimal mailbox/state/completion actor shell, and a
29-
minimal two-shell family.
30-
31-
The comparison dependencies use package aliases, so XState 5 and 6 can be
32-
loaded by the same process:
33-
34-
- `xstate-v5`: `xstate@5.32.5`, the stable v5 baseline;
35-
- `xstate-v6`: `xstate@6.0.0-alpha.31`, the latest published v6 alpha available
36-
when the harness was added.
37-
38-
All implementations use the same flat counter topology, immutable events, and
39-
terminal fence. The XState adapter uses `assign` in v5 and the v6 transition
40-
function API because `assign` is not exported by that alpha.
22+
are observed.
4123

4224
The burst benchmark reports useful counter increments per second. It enqueues
4325
one final fence event after all counter events and awaits the machine's terminal
4426
output, so the measured duration also amortizes that fence and terminal
4527
cleanup. This measures complete queue drainage, not only the enqueue time
4628
returned by `MachineRef.send`.
4729

48-
Results are informational. Compare runs on the same machine while it is idle,
49-
using the same Node.js and dependency versions. Tinybench warms each scenario
50-
before collecting samples, and the memory measurements force garbage
51-
collection before every observation. Each implementation's memory curve runs
52-
in a fresh child process so garbage from one library cannot distort another
53-
library's baseline.
54-
55-
These scenarios compare observable work, not identical internals. Effect
56-
Machine plans transitions synchronously and validates schema-backed state and
57-
events, while its running machine provisions Effect queues, fibers,
58-
synchronization, change publication, and child/invoke lifecycle machinery.
59-
XState's counter is a smaller synchronous actor. Treat the comparison as an
60-
application-level cost baseline, not a claim that the libraries provide the
61-
same runtime guarantees.
30+
Compare runs on the same machine while it is idle, using the same Node.js and
31+
dependency versions. Tinybench warms each scenario before collecting samples,
32+
and the memory measurements force garbage collection before every observation.
33+
Each memory profile runs in a fresh child process so garbage from one profile
34+
cannot distort another profile's baseline.
6235

6336
The fitted heap slope is the primary idle-capacity metric. Compare adjacent
6437
profiles to attribute retained memory: raw process to idle statechart isolates
6538
statechart machinery, two independent machines to parent-with-child isolates
6639
relationship bookkeeping, while the two observed parent-child profiles isolate
6740
registry and invoked-snapshot observation. Invoked snapshot mapping uses a
6841
direct, state-scoped delivery path; its profile measures the retained callback
69-
and mapping state rather than a general `changes` stream subscription. The
70-
Effect profiles are primitive lower bounds, not feature-equivalent competitors.
71-
The Effect throughput reference points similarly bound individual runtime
72-
operations rather than predicting a complete machine by themselves. In
73-
particular, the owner-only mutable snapshot is safe only when one process fiber
74-
owns active state writes; terminal arbitration and externally visible
75-
observation still require separate coordination.
42+
and mapping state rather than a general `changes` stream subscription.
43+
7644
Resident memory is reported as a raw diagnostic because V8 and the
7745
operating-system allocator can reuse already committed pages. The
7846
capacity-per-GiB value is a linear estimate that excludes shared process
@@ -105,9 +73,11 @@ benchmark workflow has read-only repository access; a separate trusted
10573
`workflow_run` workflow validates the uploaded JSON before receiving permission
10674
to update the comment.
10775

108-
The implementation lives in `scripts/runtime-performance.mjs`; the Effect
109-
Machine fixture is in `perf/runtime/counter.mjs`, and the comparison adapter is
110-
in `perf/runtime/xstate.mjs`. Effect runtime reference fixtures are in
111-
`perf/runtime/effect-runtime.mjs`. Add cross-library scenarios only when every
112-
implementation performs equivalent observable work and the result is consumed
113-
and checked so the JavaScript engine cannot discard it.
76+
The required pull request check rejects throughput decreases above both 15%
77+
and three times the observed process-level median absolute deviation. Heap per
78+
unit uses the same variability rule with a 20% floor. RSS remains informational
79+
because hosted-runner and allocator behavior makes it substantially noisier.
80+
81+
The implementation lives in `scripts/runtime-performance.mjs`, and the Effect
82+
Machine fixture is in `perf/runtime/counter.mjs`. Every scenario consumes and
83+
checks its result so the JavaScript engine cannot discard the measured work.

0 commit comments

Comments
 (0)