1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
name: CI
on:
push:
branches:
paths-ignore:
- '**/*.md'
pull_request:
paths-ignore:
- '**/*.md'
# Timed runs are `main`-only, so this is the way to measure a branch on
# demand without merging it first.
workflow_dispatch:
inputs:
threshold:
description: 'Regression threshold, in percent, for the delta report'
required: false
default: '15'
permissions:
contents: read
# Superseded pull-request runs are cancelled, because the scarce resource here
# is the runner pool rather than the minutes: a run for a commit that has
# already been pushed over sits in the same queue as the run somebody is
# waiting on, and this repository pushes to a branch several times before it is
# reviewed. `main` pushes and `workflow_dispatch` runs are never cancelled -
# the benchmark job's stored baseline is a chain, and a cancelled link is a
# missing comparison for the run after it.
#
# The exemption is carried by the *group* rather than by `cancel-in-progress`,
# which is a literal here on purpose. `cancel-in-progress: ${{ ... }}` is
# substituted before it is read, so a `push` rendered the string `false` and
# every non-empty string is truthy: the flag meant to protect `main` was the
# thing cancelling it, and only when two pushes landed close enough together to
# overlap, which is why it survived so long (#451). Instead, only a
# `pull_request` shares a group with anything - `github.ref` is
# `refs/pull/N/merge`, so a PR's runs supersede each other and no other PR's -
# while a `push` or a `workflow_dispatch` keys on `github.run_id` and is
# therefore alone in its group, where `cancel-in-progress` has nothing to act
# on.
concurrency:
group: ci-${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.run_id }}
cancel-in-progress: true
jobs:
# Neither cargo nor nextest can cache a test result, so a pull request that
# touches nothing a job reads still paid for that job (#600). This decides
# which of the jobs below a pull request has to run, from the files it
# changes, and the others are skipped rather than failed: `Rust checks`,
# `WebAssembly checks`, `Rust tests (<package>)`, `Native tests (<os>)` and
# `macOS Swift runtime rpath`. The benchmark jobs are not gated; on a pull
# request the only one is the compile check.
#
# It also decides which workspace packages a pull request can affect (#604),
# and the test and benchmark jobs build and run only those. Every package has
# a filter named after it that matches its own directory, the directory of
# every workspace crate it depends on - through its dependencies,
# build-dependencies and dev-dependencies, and theirs - and every file it
# pulls in through `include_str!` or `include_bytes!` from anywhere else, so
# a change to a crate selects that crate and everything that depends on it,
# directly or transitively, and nothing else (#612). `packages` is the JSON
# list of the filters that matched, or `all`; the jobs pick their package
# names out of it. `crates` is the same selection as a JSON list of package
# names only, `all` expanded, which is what the per-crate test matrix runs.
#
# Only a pull request is filtered. A `main` push and a `workflow_dispatch`
# run everything, because the benchmark baseline is a chain and `main`'s
# history is what a regression is bisected through; the filter step does not
# even run there, and each output is `true` before it reads the step.
#
# The filters err towards running a job. `everything` is the build
# definition - the manifest, the lockfile, the build script, the toolchain,
# cargo's and nextest's configuration and this workflow - and any of those
# runs every job and every package. Otherwise a job runs on every directory
# it compiles or reads, and the only exclusions are modules its target does
# not compile: the `cfg(not(target_arch = "wasm32"))` crates, modules and
# platform backends for the WebAssembly checks, and the
# `cfg(target_arch = "wasm32")` browser modules for the native jobs. With
# `some-with-excludes` a file is matched by any pattern of a filter and
# unmatched by any `!` pattern of it.
changes:
name: Changed paths
runs-on: ubuntu-latest
# The pull-request file list comes from the API, so nothing is checked out.
permissions:
contents: read
pull-requests: read
outputs:
rust: ${{ github.event_name != 'pull_request' || steps.filter.outputs.everything == 'true' || steps.filter.outputs.rust == 'true' }}
wasm: ${{ github.event_name != 'pull_request' || steps.filter.outputs.everything == 'true' || steps.filter.outputs.wasm == 'true' }}
rust_tests: ${{ github.event_name != 'pull_request' || steps.filter.outputs.everything == 'true' || steps.filter.outputs.rust_tests == 'true' }}
native_tests: ${{ github.event_name != 'pull_request' || steps.filter.outputs.everything == 'true' || steps.filter.outputs.native_tests == 'true' }}
macos_swift_rpath: ${{ github.event_name != 'pull_request' || steps.filter.outputs.everything == 'true' || steps.filter.outputs.macos_swift_rpath == 'true' }}
packages: ${{ (github.event_name != 'pull_request' || steps.filter.outputs.everything == 'true') && 'all' || steps.filter.outputs.changes }}
crates: ${{ steps.crates.outputs.crates }}
steps:
- name: Match the changed files against each job's and package's inputs
id: filter
if: github.event_name == 'pull_request'
uses: dorny/paths-filter@v4
with:
predicate-quantifier: some-with-excludes
filters: |
everything:
- 'Cargo.toml'
- 'Cargo.lock'
- 'build.rs'
- 'rust-toolchain.toml'
- '.cargo/**'
- '.config/**'
- '.github/workflows/ci.yml'
# Formatting covers every Rust file, the browser modules included;
# the lints cover every target; the scripts' tests, the SIMD
# assembly check and the web example's Node tests run here too.
rust:
- 'src/**'
- 'tests/**'
- 'benches/**'
- 'examples/**'
- 'crates/**'
- 'rustfmt.toml'
- '.github/scripts/**'
# `wasm-pack test` builds the library's units and the integration
# tests for wasm32; the native-only examples are skipped by their
# `required-features`, but a new example might not be. Only the
# sources of a native-only crate are excluded, never its manifest:
# its dependencies are still resolved for wasm32.
wasm:
- 'src/**'
- 'tests/**'
- 'crates/**'
- 'examples/**/*.rs'
- 'js/**'
- '!crates/zvidlib-aac-encoder/src/**'
- '!crates/zvidlib-hevc-encoder/src/**'
- '!src/native_audio.rs'
- '!src/native_audio/**'
- '!crates/zvidlib-hardware/src/annexb.rs'
- '!crates/zvidlib-hardware/src/nvdec.rs'
- '!crates/zvidlib-hardware/src/planar.rs'
- '!crates/zvidlib-hardware/src/readback.rs'
- '!crates/zvidlib-hardware/src/videotoolbox.rs'
- '!crates/zvidlib-hardware/src/videotoolbox_encoder.rs'
- '!crates/zvidlib-hardware/src/videotoolbox_vp9.rs'
- '!crates/zvidlib-hardware/src/windows_mf.rs'
- '!crates/zvidlib-hardware/src/windows_mf_encoder.rs'
- '!crates/zvidlib-hevc-decoder/src/decode_bench.rs'
rust_tests:
- 'src/**'
- 'tests/**'
- 'benches/**'
- 'examples/**'
- 'crates/**'
- '!src/wasm_api.rs'
- '!src/web_audio_decoder.rs'
- '!src/web_decoder.rs'
- '!src/web_encoder.rs'
- '!src/web_previews.rs'
native_tests:
- 'src/**'
- 'tests/**'
- 'benches/**'
- 'examples/**'
- 'crates/**'
- '!src/wasm_api.rs'
- '!src/web_audio_decoder.rs'
- '!src/web_decoder.rs'
- '!src/web_encoder.rs'
- '!src/web_previews.rs'
# A release build of the library and the one test that inspects
# what it links.
macos_swift_rpath:
- 'src/**'
- 'crates/**'
- 'tests/macos_swift_runtime_rpath.rs'
- '!src/wasm_api.rs'
- '!src/web_audio_decoder.rs'
- '!src/web_decoder.rs'
- '!src/web_encoder.rs'
- '!src/web_previews.rs'
# One filter per workspace package, named after it.
zvidlib:
- 'src/**'
- 'tests/**'
- 'benches/**'
- 'examples/**'
- 'crates/zvidlib-aac-encoder/**'
- 'crates/zvidlib-av1/**'
- 'crates/zvidlib-av1-decoder/**'
- 'crates/zvidlib-av1-encoder/**'
- 'crates/zvidlib-av1-syntax/**'
- 'crates/zvidlib-bench-support/**'
- 'crates/zvidlib-build/**'
- 'crates/zvidlib-color/**'
- 'crates/zvidlib-container/**'
- 'crates/zvidlib-core/**'
- 'crates/zvidlib-hardware/**'
- 'crates/zvidlib-hevc/**'
- 'crates/zvidlib-hevc-decoder/**'
- 'crates/zvidlib-hevc-encoder/**'
- 'crates/zvidlib-hevc-syntax/**'
- 'crates/zvidlib-opus/**'
- 'crates/zvidlib-opus-syntax/**'
- 'crates/zvidlib-vorbis-decoder/**'
- 'crates/zvidlib-vorbis-encoder/**'
- 'crates/zvidlib-vorbis-syntax/**'
- 'crates/zvidlib-vp8/**'
- 'crates/zvidlib-vp9-decoder/**'
- 'crates/zvidlib-vp9-encoder/**'
- 'crates/zvidlib-vp9-syntax/**'
zvidlib-aac-encoder:
- 'crates/zvidlib-aac-encoder/**'
- 'crates/zvidlib-core/**'
zvidlib-av1:
- 'crates/zvidlib-av1/**'
- 'crates/zvidlib-av1-syntax/**'
- 'crates/zvidlib-bench-support/**'
- 'crates/zvidlib-color/**'
- 'crates/zvidlib-container/**'
- 'crates/zvidlib-core/**'
- 'crates/zvidlib-opus-syntax/**'
- 'crates/zvidlib-vorbis-syntax/**'
zvidlib-av1-decoder:
- 'crates/zvidlib-av1/**'
- 'crates/zvidlib-av1-decoder/**'
- 'crates/zvidlib-av1-encoder/**'
- 'crates/zvidlib-av1-syntax/**'
- 'crates/zvidlib-bench-support/**'
- 'crates/zvidlib-color/**'
- 'crates/zvidlib-container/**'
- 'crates/zvidlib-core/**'
- 'crates/zvidlib-opus-syntax/**'
- 'crates/zvidlib-vorbis-syntax/**'
- 'examples/media/**'
zvidlib-av1-encoder:
- 'crates/zvidlib-av1/**'
- 'crates/zvidlib-av1-decoder/**'
- 'crates/zvidlib-av1-encoder/**'
- 'crates/zvidlib-av1-syntax/**'
- 'crates/zvidlib-bench-support/**'
- 'crates/zvidlib-color/**'
- 'crates/zvidlib-container/**'
- 'crates/zvidlib-core/**'
- 'crates/zvidlib-opus-syntax/**'
- 'crates/zvidlib-vorbis-syntax/**'
zvidlib-av1-syntax:
- 'crates/zvidlib-av1-syntax/**'
- 'crates/zvidlib-core/**'
zvidlib-bench-support:
- 'crates/zvidlib-bench-support/**'
- 'crates/zvidlib-core/**'
zvidlib-build:
- 'crates/zvidlib-build/**'
zvidlib-color:
- 'crates/zvidlib-color/**'
- 'crates/zvidlib-core/**'
zvidlib-container:
- 'crates/zvidlib-av1-decoder/**'
- 'crates/zvidlib-av1-syntax/**'
- 'crates/zvidlib-color/**'
- 'crates/zvidlib-container/**'
- 'crates/zvidlib-core/**'
- 'crates/zvidlib-opus-syntax/**'
- 'crates/zvidlib-vorbis-syntax/**'
- 'crates/zvidlib-vp9-decoder/tests/fixtures/vp9_bbb_256x144.mp4'
- 'examples/media/**'
zvidlib-core:
- 'crates/zvidlib-core/**'
zvidlib-hardware:
- 'crates/zvidlib-av1-syntax/**'
- 'crates/zvidlib-build/**'
- 'crates/zvidlib-color/**'
- 'crates/zvidlib-container/**'
- 'crates/zvidlib-core/**'
- 'crates/zvidlib-hardware/**'
- 'crates/zvidlib-hevc/**'
- 'crates/zvidlib-hevc-decoder/**'
- 'crates/zvidlib-hevc-encoder/**'
- 'crates/zvidlib-hevc-syntax/**'
- 'crates/zvidlib-opus-syntax/**'
- 'crates/zvidlib-vorbis-syntax/**'
- 'crates/zvidlib-vp9-decoder/**'
- 'crates/zvidlib-vp9-syntax/**'
zvidlib-hevc:
- 'crates/zvidlib-core/**'
- 'crates/zvidlib-hevc/**'
- 'crates/zvidlib-hevc-syntax/**'
zvidlib-hevc-decoder:
- 'crates/zvidlib-av1-syntax/**'
- 'crates/zvidlib-bench-support/**'
- 'crates/zvidlib-build/**'
- 'crates/zvidlib-color/**'
- 'crates/zvidlib-container/**'
- 'crates/zvidlib-core/**'
- 'crates/zvidlib-hardware/**'
- 'crates/zvidlib-hevc/**'
- 'crates/zvidlib-hevc-decoder/**'
- 'crates/zvidlib-hevc-encoder/**'
- 'crates/zvidlib-hevc-syntax/**'
- 'crates/zvidlib-opus-syntax/**'
- 'crates/zvidlib-vorbis-syntax/**'
- 'crates/zvidlib-vp9-syntax/**'
- 'examples/media/**'
zvidlib-hevc-encoder:
- 'crates/zvidlib-av1-syntax/**'
- 'crates/zvidlib-bench-support/**'
- 'crates/zvidlib-build/**'
- 'crates/zvidlib-color/**'
- 'crates/zvidlib-container/**'
- 'crates/zvidlib-core/**'
- 'crates/zvidlib-hardware/**'
- 'crates/zvidlib-hevc/**'
- 'crates/zvidlib-hevc-decoder/**'
- 'crates/zvidlib-hevc-encoder/**'
- 'crates/zvidlib-hevc-syntax/**'
- 'crates/zvidlib-opus-syntax/**'
- 'crates/zvidlib-vorbis-syntax/**'
- 'crates/zvidlib-vp9-syntax/**'
- 'examples/media/**'
zvidlib-hevc-syntax:
- 'crates/zvidlib-hevc-syntax/**'
zvidlib-opus:
- 'crates/zvidlib-av1-syntax/**'
- 'crates/zvidlib-container/**'
- 'crates/zvidlib-core/**'
- 'crates/zvidlib-opus/**'
- 'crates/zvidlib-opus-syntax/**'
- 'crates/zvidlib-vorbis-syntax/**'
zvidlib-opus-syntax:
- 'crates/zvidlib-core/**'
- 'crates/zvidlib-opus-syntax/**'
zvidlib-vorbis-decoder:
- 'crates/zvidlib-core/**'
- 'crates/zvidlib-vorbis-decoder/**'
- 'crates/zvidlib-vorbis-syntax/**'
zvidlib-vorbis-encoder:
- 'crates/zvidlib-av1-syntax/**'
- 'crates/zvidlib-bench-support/**'
- 'crates/zvidlib-container/**'
- 'crates/zvidlib-core/**'
- 'crates/zvidlib-opus-syntax/**'
- 'crates/zvidlib-vorbis-decoder/**'
- 'crates/zvidlib-vorbis-encoder/**'
- 'crates/zvidlib-vorbis-syntax/**'
zvidlib-vorbis-syntax:
- 'crates/zvidlib-core/**'
- 'crates/zvidlib-vorbis-syntax/**'
zvidlib-vp8:
- 'crates/zvidlib-av1-syntax/**'
- 'crates/zvidlib-bench-support/**'
- 'crates/zvidlib-build/**'
- 'crates/zvidlib-color/**'
- 'crates/zvidlib-container/**'
- 'crates/zvidlib-core/**'
- 'crates/zvidlib-hardware/**'
- 'crates/zvidlib-hevc-syntax/**'
- 'crates/zvidlib-opus-syntax/**'
- 'crates/zvidlib-vorbis-syntax/**'
- 'crates/zvidlib-vp8/**'
- 'crates/zvidlib-vp9-syntax/**'
zvidlib-vp9-decoder:
- 'crates/zvidlib-av1-syntax/**'
- 'crates/zvidlib-bench-support/**'
- 'crates/zvidlib-build/**'
- 'crates/zvidlib-color/**'
- 'crates/zvidlib-container/**'
- 'crates/zvidlib-core/**'
- 'crates/zvidlib-hardware/**'
- 'crates/zvidlib-hevc-syntax/**'
- 'crates/zvidlib-opus-syntax/**'
- 'crates/zvidlib-vorbis-syntax/**'
- 'crates/zvidlib-vp9-decoder/**'
- 'crates/zvidlib-vp9-syntax/**'
zvidlib-vp9-encoder:
- 'crates/zvidlib-av1-syntax/**'
- 'crates/zvidlib-bench-support/**'
- 'crates/zvidlib-build/**'
- 'crates/zvidlib-color/**'
- 'crates/zvidlib-container/**'
- 'crates/zvidlib-core/**'
- 'crates/zvidlib-hardware/**'
- 'crates/zvidlib-hevc-syntax/**'
- 'crates/zvidlib-opus-syntax/**'
- 'crates/zvidlib-vorbis-syntax/**'
- 'crates/zvidlib-vp9-decoder/**'
- 'crates/zvidlib-vp9-encoder/**'
- 'crates/zvidlib-vp9-syntax/**'
zvidlib-vp9-syntax:
- 'crates/zvidlib-color/**'
- 'crates/zvidlib-core/**'
- 'crates/zvidlib-vp9-syntax/**'
# A matrix cannot expand `all`, and `packages` also names the job
# filters that matched, so this narrows it to package names: every one in
# `WORKSPACE` on a `main` push, a `workflow_dispatch` or a change to the
# build definition, and otherwise the ones whose filter matched.
# `WORKSPACE` is every workspace package, one per line.
- name: List the packages whose tests run
id: crates
env:
PACKAGES: ${{ (github.event_name != 'pull_request' || steps.filter.outputs.everything == 'true') && 'all' || steps.filter.outputs.changes }}
WORKSPACE: |
zvidlib
zvidlib-aac-encoder
zvidlib-av1
zvidlib-av1-decoder
zvidlib-av1-encoder
zvidlib-av1-syntax
zvidlib-bench-support
zvidlib-build
zvidlib-color
zvidlib-container
zvidlib-core
zvidlib-hardware
zvidlib-hevc
zvidlib-hevc-decoder
zvidlib-hevc-encoder
zvidlib-hevc-syntax
zvidlib-opus
zvidlib-opus-syntax
zvidlib-vorbis-decoder
zvidlib-vorbis-encoder
zvidlib-vorbis-syntax
zvidlib-vp8
zvidlib-vp9-decoder
zvidlib-vp9-encoder
zvidlib-vp9-syntax
run: |
workspace="$(jq -cnR '[inputs | select(length > 0)]' <<<"$WORKSPACE")"
if [ "$PACKAGES" = all ]; then
crates="$workspace"
else
crates="$(jq -c --argjson workspace "$workspace" 'map(select(IN($workspace[])))' <<<"$PACKAGES")"
fi
echo "crates=$crates" | tee -a "$GITHUB_OUTPUT"
# The Rust tests used to run here and in `native-platforms` as one
# `cargo test` step after another, each a separate binary that waited for the
# one before it, and the two jobs took twelve minutes apiece (#595). They are
# now run with cargo-nextest, one job per workspace package on Linux, in
# `rust-tests` below. What is left here is the part of the old job that is
# not a Rust test: formatting, lints and the scripts the workflow uses.
rust:
name: Rust checks
needs: changes
if: needs.changes.outputs.rust == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
run: rustup show
# This job rebuilt every dependency from a cold `target/` on every run
# before this step existed (#352). The cache is keyed on the job, the
# runner, the toolchain and the manifest, so the `[profile.bench]`
# fat-LTO artifacts and the `[profile.dev]` ones never share an entry;
# `cache-on-failure` keeps a failed run's dependency build, since the
# dependencies are not what failed.
- name: Cache cargo registry and build artifacts
uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: Install native audio dependencies
timeout-minutes: 15
run: python3 .github/scripts/apt_install.py libasound2-dev
- name: Check formatting
run: cargo fmt --all -- --check
# Clippy type-checks every target on its way to linting it, so a separate
# `cargo check --all-targets` before it only did the same work twice.
# `--workspace`, because without it cargo only reaches the root package.
- name: Lint native target
run: cargo clippy --workspace --all-targets --features native -- -D warnings
# The codec features (#635) are additive, and each has to build on its
# own: the lint above only sees every feature at once, where a missing
# `#[cfg(feature = "...")]` never shows. Each codec feature and shorthand
# alone, with and without the platform hardware backends, plus none at
# all. The codec crates themselves are built with their hardware off too.
- name: Lint each codec feature on its own
run: |
set -euo pipefail
cargo clippy -p zvidlib --all-targets --no-default-features --features native -- -D warnings
for feature in av1-decoder av1-encoder vp8-decoder vp8-encoder vp9-decoder vp9-encoder \
hevc-decoder hevc-encoder opus-decoder opus-encoder \
vorbis-decoder vorbis-encoder aac-decoder aac-encoder \
av1 vp8 vp9 hevc opus vorbis aac all; do
for hardware in "" hardware; do
echo "::group::$feature $hardware"
cargo clippy -p zvidlib --all-targets --no-default-features \
--features "native $feature $hardware" -- -D warnings
echo "::endgroup::"
done
done
cargo clippy -p zvidlib-vp8 -p zvidlib-vp9-decoder -p zvidlib-vp9-encoder \
-p zvidlib-hevc-decoder -p zvidlib-hevc-encoder \
--all-targets --no-default-features -- -D warnings
# `criterion_baseline.py` is where a benchmark regression threshold is
# justified from, and it runs in a job that takes two hours to reach it.
# Standard library only, so this needs no install step. Its `table`
# subcommand is covered here too: that path's failure mode is a plausible
# table with a column silently missing rather than a crash - the arm name
# it matched drifted from the one `SimdIsa` prints and every SSE4.1
# measurement was dropped - and nothing else in CI executes it.
#
# The staleness report used to run here and is now `baseline-staleness.yml`:
# its whole input is `benches/README.md`, and the `paths-ignore` above
# meant the changes that edit that file were exactly the changes that
# skipped it (#449). The unit tests of the script it runs stay here,
# since those depend on the script rather than on the tables.
- name: Test the benchmark baseline script
run: python3 -m unittest discover -s .github/scripts -v
# The AV1 SIMD kernels being bit-exact is not the same as their being
# fast, and the `av1_simd` unit tests only prove the first.
# `#[target_feature]` widens the function it is written on and nothing it
# calls, so an `av1_simd` kernel the inliner declines to absorb into its
# wrapper is compiled at the baseline instruction set and runs several
# times slower than the scalar code it replaces, with every test still
# passing (#336). This greps that out of the emitted assembly. One x86_64
# host is enough and this job is one: on the matrix's aarch64 macOS runner
# NEON is in the baseline, so a kernel that lost its attribute still
# compiles to NEON and leaves no trace. CI runs no x86_64 macOS host
# (#593), and one would only repeat what runs here.
- name: Check the SIMD kernels kept their target feature
run: python3 .github/scripts/check_simd_target_features.py build
# The web example makes the same scrubbing decisions `native_gl` does - the random-access
# restart, the measured stride, the walk's next step - and its copy of them had nothing
# asserting it (#380). Node's built-in runner, so this needs no install step and no
# dependency the example does not already have. A glob rather than `scrub.test.js`: which
# bundled sample the page opens is a decision of the same kind and has its own
# `samples.test.js` beside it (#441), and naming one file is how the second one would go
# unrun. Not the bare directory - `node --test <dir>` resolves it as a module and fails.
- name: Test the web example's pure decisions
run: node --test 'examples/web_canvas/*.test.js'
# The browser build shares nothing with the native one - another target,
# another feature set, another `target/` subtree - so it runs beside the other
# jobs instead of at the end of one of them.
wasm:
name: WebAssembly checks
needs: changes
if: needs.changes.outputs.wasm == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
run: rustup show
- name: Cache cargo registry and build artifacts
uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: Check WebAssembly target
run: cargo check --target wasm32-unknown-unknown --features web
# Each codec feature (#635) on its own in the browser build, as the
# `rust` job lints them natively.
- name: Check each codec feature on its own for WebAssembly
run: |
set -euo pipefail
for feature in "" av1-decoder av1-encoder vp8-decoder vp8-encoder vp9-decoder vp9-encoder \
hevc-decoder hevc-encoder opus-decoder opus-encoder \
vorbis-decoder vorbis-encoder aac-decoder aac-encoder \
av1 vp8 vp9 hevc opus vorbis aac all; do
echo "::group::web $feature"
cargo check --target wasm32-unknown-unknown -p zvidlib --lib --tests --examples \
--no-default-features --features "web $feature"
echo "::endgroup::"
done
# The published musl binary rather than `cargo install wasm-pack`, which
# compiled the tool and its dependency tree from source on every run for
# a build that is not this crate's. The version is still pinned, and the
# download is a release asset of the same tag `--version` named.
- name: Install wasm-pack
env:
WASM_PACK_VERSION: '0.15.0'
run: |
target="wasm-pack-v${WASM_PACK_VERSION}-x86_64-unknown-linux-musl"
mkdir -p "$RUNNER_TEMP/wasm-pack"
curl -sSfL "https://github.com/rustwasm/wasm-pack/releases/download/v${WASM_PACK_VERSION}/${target}.tar.gz" \
| tar -xz --strip-components=1 -C "$RUNNER_TEMP/wasm-pack" "${target}/wasm-pack"
"$RUNNER_TEMP/wasm-pack/wasm-pack" --version
echo "$RUNNER_TEMP/wasm-pack" >> "$GITHUB_PATH"
- name: Build browser package
run: wasm-pack build --target web --out-dir pkg --features web
- name: Test in headless Chrome
run: wasm-pack test --headless --chrome --features web
# Every package's tests on Linux, one job per package the pull request can
# affect (#612). cargo-nextest runs each test as its own process across every
# binary of the package at once, and the packages run side by side as jobs of
# their own, so a change to one codec builds and runs that codec's tests and
# those of the crates that depend on it, and not the whole suite. `changes`
# decides which packages those are; a `main` push and a `workflow_dispatch`
# run every one. The root package depends on every library crate, so a pull
# request that runs this job always runs its `zvidlib` leg, and the matrix is
# never empty. The packages used to be built into a single archive dealt out
# over a matrix of shards (#595), and a shard ran tests from every package in
# it whether or not the change could reach them.
#
# Each leg runs every test target of its package - `--lib`, `--tests` and
# the examples that set `test = true` - so a new test file runs without a
# change here (#616). What a test checks is written in its own doc comment,
# and a test that cannot run on some host is kept off it by `#[cfg]`,
# `#[ignore]` or `.config/nextest.toml`, never by leaving it out of this job.
# `.config/nextest.toml` also keeps the hardware and wall-clock tests from
# sharing the machine, which one step per binary used to guarantee.
# `--no-tests=warn`, because a package such as `zvidlib-build` has no test of
# its own, and running none is then correct rather than a failure.
rust-tests:
name: Rust tests (${{ matrix.package }})
needs: changes
if: needs.changes.outputs.rust_tests == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
package: ${{ fromJSON(needs.changes.outputs.crates) }}
env:
ZVIDLIB_OPUS_TEST_VECTORS: ${{ github.workspace }}/opus_newvectors
# libvpx must decode the native VP8 encoder's WebM to exactly the pictures
# zvidlib's decoder does, and ffmpeg must seek it through its Cues (#530).
# The test skips itself without ffmpeg's libvpx decoder elsewhere, so this
# job installs ffmpeg below and requires it.
ZVIDLIB_REQUIRE_LIBVPX: "1"
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
run: rustup show
# One cache per package, since each leg builds a different slice of the
# workspace and a shared key would have the legs overwrite each other.
- name: Cache cargo registry and build artifacts
uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.package }}
cache-on-failure: true
# The VP9 encoder's tests require libvpx and FFmpeg's own VP9 decoder to
# reproduce its reconstruction exactly (#528), and the cover-art and WebM
# tests check their output with ffprobe. They skip themselves where
# ffmpeg is missing, so it is installed before they run, but only for the
# packages whose tests run ffmpeg or ffprobe: it is a hundred packages,
# and the larger the install, the more often it hits a stalled mirror. A
# package with a new test that runs either must be added to this list. A
# stalled apt mirror once held these steps for hours (#647): the script
# kills an apt-get that runs for four minutes and retries it from another
# mirror, and the step limit fails the job if every attempt stalls.
- name: Install native audio dependencies and ffmpeg
timeout-minutes: 15
run: >-
python3 .github/scripts/apt_install.py libasound2-dev
${{ contains(fromJSON('["zvidlib", "zvidlib-av1", "zvidlib-av1-encoder", "zvidlib-container", "zvidlib-hevc-encoder", "zvidlib-opus", "zvidlib-vorbis-encoder", "zvidlib-vp8", "zvidlib-vp9-encoder"]'), matrix.package) && 'ffmpeg' || '' }}
# A release binary rather than `cargo install`, which would compile the
# runner and its dependency tree on every run.
- name: Install cargo-nextest
uses: taiki-e/install-action@v2
with:
tool: nextest
# The twelve official Opus decoder test vectors (RFC 8251) are 75 MB, so
# they are downloaded rather than committed, and cached so most runs do
# not download them at all. The key names the file; it never changes.
# Only `opus_codec` reads them.
- name: Cache the Opus RFC 8251 test vectors
id: opus-test-vectors
if: matrix.package == 'zvidlib-opus'
uses: actions/cache@v4
with:
path: opus_newvectors
key: opus-testvectors-rfc8251-workspace
- name: Download the Opus RFC 8251 test vectors
if: matrix.package == 'zvidlib-opus' && steps.opus-test-vectors.outputs.cache-hit != 'true'
run: |
curl -sSfL https://opus-codec.org/static/testvectors/opus_testvectors-rfc8251.tar.gz \
| tar -xz
# `native` is a feature of the root package only, and naming a feature
# no selected package declares is an error.
- name: Run this package's tests
env:
PACKAGE: ${{ matrix.package }}
run: |
features=()
if [ "$PACKAGE" = zvidlib ]; then
features=(--features native)
fi
cargo nextest run --profile ci --no-tests=warn -p "$PACKAGE" "${features[@]}" \
--lib --tests --examples
# The VP9 decoder is held bit-exact to libvpx on every profile 0 test
# vector libvpx's own test suite decodes (#527): hidden frames,
# superframes, show_existing_frame, intra-only frames, scaled reference
# frames, tiles, frame sizes from 2x2 to 1920x1080 and so on. The vectors
# and their per-frame MD5s come from the WebM project's test-data bucket,
# about 27 MB in all, so they are cached between runs, keyed on the list
# of names the test reads. One host is enough: this one runs the widest
# vector kernels it detects, and every other instruction set is held to
# the scalar reference by the `vp9_simd` unit tests and the per-ISA
# fixture decode in the library units, which every native host runs
# (#570).
#
# The test is a unit of `zvidlib-vp9-decoder`, `#[ignore]`d for every
# other run, so that package's leg runs it on its own with
# `--run-ignored only` and a filter that names it - the library's other
# ignored tests are measurement sweeps.
- name: Cache the libvpx VP9 test vectors
if: matrix.package == 'zvidlib-vp9-decoder'
uses: actions/cache@v4
with:
path: vp9-test-vectors
key: libvpx-vp9-test-vectors-${{ hashFiles('crates/zvidlib-vp9-decoder/tests/fixtures/libvpx_vp9_test_vectors.txt') }}
- name: Download the libvpx VP9 test vectors
if: matrix.package == 'zvidlib-vp9-decoder'
run: |
mkdir -p vp9-test-vectors
while read -r name; do
for file in "$name" "$name.md5"; do
if [ ! -f "vp9-test-vectors/$file" ]; then
curl -sSfL --retry 3 -o "vp9-test-vectors/$file" \
"https://storage.googleapis.com/downloads.webmproject.org/test_data/libvpx/$file"
fi
done
done < crates/zvidlib-vp9-decoder/tests/fixtures/libvpx_vp9_test_vectors.txt
- name: Test VP9 decoding against the libvpx test vectors
if: matrix.package == 'zvidlib-vp9-decoder'
env:
ZVIDLIB_VP9_VECTORS: ${{ github.workspace }}/vp9-test-vectors
run: |
cargo nextest run --profile ci -p zvidlib-vp9-decoder --lib \
--run-ignored only -E 'test(=vp9_dec::tests::libvpx_test_vectors)'
# Benchmarks are measured, not gated. On a pull request the suite is only
# compiled: `cargo bench --no-run` costs one build and catches bench-code
# rot, which is how benchmark suites usually die - the code stops compiling
# against the crate and nobody notices for months. It deliberately does not
# time anything on a pull request. GitHub's shared runners vary by an order of
# magnitude more than the regressions worth catching, so a PR gate on those
# timings would fail on noise until someone disabled it.
#
# Timed runs happen on `main` pushes and `workflow_dispatch`, where successive
# runs are comparable enough to be worth storing: each one saves a baseline
# artifact and reports its deltas against the previous one in the job summary.
#
# The compile check is one job and the timed run is one job *per bench
# target*, because the two have opposite shapes. Compiling is a single cargo
# invocation whose cost is almost entirely the shared crate build, so fifteen
# copies of it would pay that fifteen times for one result. Measuring is fifteen
# independent programs that share nothing but the binary they were built
# from, and running them on one runner made the job's wall clock their sum:
# on `main` push 33834381334 the timed step spanned 03:48:55Z to 04:26Z, and
# `hevc_encode` (11m24s) plus `av1_encode` (10m29s) were more than half of it
# (#459). Fanned out, the wall clock is the slowest single target instead.
benchmark-compile:
name: Benchmarks (compile only)
needs: changes
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
# `[profile.bench]` is `lto = "fat"` with `codegen-units = 1`, so the build
# alone is slow even without timing anything.
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
run: rustup show
# This job rebuilt every dependency from a cold `target/` on every run
# before this step existed (#352). The cache is keyed on the job, the
# runner, the toolchain and the manifest, so the `[profile.bench]`
# fat-LTO artifacts and the `[profile.dev]` ones never share an entry;
# `cache-on-failure` keeps a failed run's dependency build, since the
# dependencies are not what failed.
- name: Cache cargo registry and build artifacts
uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: Install native audio dependencies
timeout-minutes: 15
run: python3 .github/scripts/apt_install.py libasound2-dev
# Every `[[bench]]` target of every package the pull request can affect,
# not one of them: naming one would let the others rot unnoticed, which
# is the exact failure this job exists to catch. A package `changes` did
# not select cannot have stopped compiling, so it is left out. The root
# package depends on every library crate, so a change to any Rust source
# selects it; a pull request that selects none has nothing to compile.
- name: Compile the benchmark suite
env:
PACKAGES: ${{ needs.changes.outputs.packages }}
run: |
packages=()
benches() {
if [ "$PACKAGES" = all ] || jq -e --arg p "$1" 'index($p) != null' <<<"$PACKAGES" >/dev/null; then
packages+=(-p "$1")
fi
}
benches zvidlib
benches zvidlib-av1
benches zvidlib-av1-decoder
benches zvidlib-av1-encoder
benches zvidlib-hevc-decoder
benches zvidlib-hevc-encoder
benches zvidlib-vorbis-encoder
benches zvidlib-vp8
benches zvidlib-vp9-decoder
benches zvidlib-vp9-encoder
if [ "${#packages[@]}" -eq 0 ]; then
echo "this pull request cannot affect any benchmark"
exit 0
fi
cargo bench --features native --benches --no-run "${packages[@]}"
benchmarks:
name: Benchmarks (${{ matrix.bench }})
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
# One target failing still leaves the other fourteen measured, and the
# report job below refuses to build a baseline out of a partial suite
# anyway - so cancelling the survivors would only cost the log that says
# which ones were fine.
fail-fast: false
matrix:
# Slowest first, because a pool that cannot start all fifteen at once
# starts them in this order and the wall clock is whatever finishes
# last.
include:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
run: rustup show
- name: Cache cargo registry and build artifacts
uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: Install native audio dependencies
timeout-minutes: 15
run: python3 .github/scripts/apt_install.py libasound2-dev
- name: Compile ${{ matrix.bench }}
run: cargo bench -p ${{ matrix.package }} --bench ${{ matrix.bench }} --no-run
- name: Run ${{ matrix.bench }}
env:
# The whole-frame 1080p groups are the ones that prove the
# crate-wide SIMD override reaches the HEVC kernels on real decode
# work. They are opt-in because they are slow, which matters in an
# edit loop and not here.
ZVIDLIB_BENCH_LARGE: '1'
# `tee` would otherwise swallow a panicking benchmark's exit status;
# Actions runs `run:` under `bash -e` without pipefail.
#
# `target/criterion` is cleared first because the restored build cache
# brings back a *skeleton* of it and nothing else: `rust-cache` walks
# any directory under `target/` that is not a profile directory and
# deletes every file it finds, leaving the directories. Criterion then
# sees `<id>/base/` exists, tries to load `<id>/base/sample.json` for
# the previous-run comparison, and logs
# `Criterion.rs ERROR: ... No such file or directory` - 268 of them in
# run 33834381334, one per benchmark plus one unrelated CUDA line
# (#459). Deleting the skeleton makes every measurement a first one,
# which is what it always was.
run: |
set -o pipefail
rm -rf target/criterion
cargo bench -p ${{ matrix.package }} --bench ${{ matrix.bench }} 2>&1 \
| tee "bench-${{ matrix.bench }}.log"
# `bench_across_isas` runs one arm per entry in `zvidlib::simd::available()`,
# so a runner without AVX2 has no `avx2` arm rather than reporting scalar
# numbers under a vector label. Recording the model per target is what
# keeps a merged baseline attributable: the fifteen targets are fifteen
# runners now, and an arm that vanished because the pool changed looks
# exactly like an arm that never existed.
- name: Record this target's host
run: |
model="$(grep -m1 '^model name' /proc/cpuinfo | cut -d: -f2- | xargs)"
echo "$model ($RUNNER_OS/$RUNNER_ARCH)" > "host-${{ matrix.bench }}.txt"
# Only the two files `criterion_baseline.py collect` reads, not the
# rendered HTML next to them: the report job reassembles a
# `target/criterion` tree out of these, and the reports would be tens of
# megabytes of artifact for a script that never opens them.
# `hevc_hardware` measures nothing on a runner with no hardware decoder,
# so the tree can legitimately be empty here.
- name: Stage this target's criterion output
run: |
mkdir -p upload target/criterion
cp "bench-${{ matrix.bench }}.log" "host-${{ matrix.bench }}.txt" upload/
find target/criterion -path '*/new/*' \
\( -name estimates.json -o -name benchmark.json \) \
-exec cp --parents {} upload/ \;
- name: Upload this target's criterion output
uses: actions/upload-artifact@v4
with:
name: criterion-partial-${{ github.run_id }}-${{ matrix.bench }}
path: upload/
retention-days: 1
# The half of the old single job that was never per-target: one baseline, one
# delta report and one stored artifact for the whole suite, so the chain a
# `main` run compares against keeps the shape it had before the fan-out.
#
# `needs: benchmarks` is deliberately strict. A baseline assembled from
# whichever targets happened to succeed would drop the rest silently, and the
# next run's delta report would read that absence as benchmarks that
# disappeared rather than as a job that failed.
benchmark-report:
name: Benchmark report
if: github.event_name != 'pull_request'
needs: benchmarks
runs-on: ubuntu-latest
# `actions: read` is for listing and downloading the previous run's baseline
# artifact through the API; `actions/download-artifact` only reaches within
# a single run.
permissions:
contents: read
actions: read
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
# `merge-multiple` unpacks all fifteen artifacts into one tree. The criterion
# paths cannot collide across targets - two bench targets sharing a group
# name is what `tests/bench_group_names_are_unique.rs` forbids (#414) -
# and the per-target log and host files carry the target in their names.
- name: Download this run's per-target output
uses: actions/download-artifact@v4
with:
pattern: criterion-partial-${{ github.run_id }}-*
merge-multiple: true
path: partials
- name: Reassemble the suite's criterion tree and log
run: |
rm -rf target/criterion
mkdir -p target
mv partials/target/criterion target/criterion
# Alphabetical by target, which is the order `cargo bench` walked
# them in when this was one job, so the concatenated log reads the
# same way the old `bench.log` did.
cat partials/bench-*.log > bench.log
- name: Report the hosts and their instruction sets
run: |
{
echo '## Benchmark hosts'
echo
echo 'Each bench target runs on its own runner, so one baseline now'
echo 'spans several hosts. `benches/README.md` names the model each'
echo 'committed table was measured on, so the models have to be in'
echo "the run's own record rather than reconstructed afterwards."
echo
echo '| bench target | host CPU model |'
echo '| --- | --- |'
for host_file in partials/host-*.txt; do
target="${host_file#partials/host-}"
printf '| `%s` | %s |\n' "${target%.txt}" "$(cat "$host_file")"
done
echo
echo '```'
grep -hE '^# (host instruction sets|widest detected|dispatch site)' bench.log \
| sort -u || echo 'no instruction-set log found in the benchmark output'
echo '```'
echo
} >> "$GITHUB_STEP_SUMMARY"
# Every distinct host, not the first one: the baseline is a merge across
# fifteen runners, and naming one of them would attribute the whole suite to
# a machine that measured a fifteenth of it.
- name: Collect this run's baseline
run: |
# `awk` rather than `paste -sd '; '`, which cycles its delimiter
# *characters* and joins three hosts as `a;b c`.
hosts="$(cat partials/host-*.txt | sort -u \
| awk 'NR > 1 { printf "; " } { printf "%s", $0 } END { print "" }')"
python3 .github/scripts/criterion_baseline.py collect \
--criterion-dir target/criterion \
--out baseline/criterion-baseline.json \
--commit "$GITHUB_SHA" \
--run-url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \
--host "$hosts"
# The newest non-expired baseline uploaded by a `main` run. Absent on the
# very first run, and after the 90-day retention window lapses; the
# comparison step reports "no previous baseline" rather than failing.
- name: Download the previous main baseline
env:
GH_TOKEN: ${{ github.token }}
run: |
id="$(gh api "repos/$GITHUB_REPOSITORY/actions/artifacts?name=criterion-baseline-main&per_page=100" \
--jq '[.artifacts[] | select(.expired == false) | select(.workflow_run.head_branch == "main")][0].id')"
if [ -z "$id" ] || [ "$id" = "null" ]; then
echo "no previous baseline artifact; this run establishes one"
exit 0
fi
if ! gh api "repos/$GITHUB_REPOSITORY/actions/artifacts/$id/zip" > previous.zip; then
echo "could not download baseline artifact $id; reporting without a comparison"
rm -f previous.zip
exit 0
fi
unzip -o -j previous.zip -d previous
- name: Compare against the previous baseline
run: |
python3 .github/scripts/criterion_baseline.py compare \
--previous previous/criterion-baseline.json \
--current baseline/criterion-baseline.json \
--threshold '${{ inputs.threshold || 15 }}' \
--out delta.md
cat delta.md >> "$GITHUB_STEP_SUMMARY"
# `push` only, not `workflow_dispatch`: the stored chain is meant to be
# successive states of `main`, and a dispatch can run on any branch. A
# dispatch still gets its delta report, it just does not become the
# baseline the next `main` run is measured against.
- name: Upload this run's baseline
if: github.event_name == 'push'
uses: actions/upload-artifact@v4
with:
name: criterion-baseline-main
path: baseline/criterion-baseline.json
retention-days: 90
# This one is per-run rather than a chain, so it carries the collected
# baseline too. A `workflow_dispatch` run deliberately does not become the
# stored `main` baseline above, and without a copy here its numbers would
# be unrecoverable off the runner - which is exactly what refreshing the
# committed table in `benches/README.md` needs from a dispatched run.
- name: Upload the full benchmark log
uses: actions/upload-artifact@v4
with:
name: criterion-report-${{ github.run_id }}
path: |
bench.log
baseline/criterion-baseline.json
target/criterion/**/new/estimates.json
retention-days: 30
# The Windows and macOS halves of what `rust-tests` does for Linux (#595,
# #612): lint the platform's `cfg` half of the tree, then run the tests of
# the packages the pull request can affect, each named with `-p`.
#
# `macos-latest` is aarch64, and Apple silicon's dyld shared cache resolves
# the Swift runtime the `apple-cf` bridge references whether or not the
# binary carries an rpath for it. x86_64 macOS does not, so a missing rpath is
# a launch failure there and invisible here (#327). x86_64 macOS is still
# supported, but CI no longer runs an Intel macOS leg (#593), so that
# regression is only caught when someone runs the tests on an Intel Mac by
# hand.
#
# Every test target of each package the pull request can affect runs, as on
# Linux, so each test runs on every host its `#[cfg]` builds it for: the
# hardware tests reach VideoToolbox, Media Foundation and NVDEC here, and the
# platform AAC tests the codecs Linux does not have. The selected packages
# run in one `cargo nextest run` per host rather than a job per package,
# because each job here would repeat the platform lint and these runners are
# the scarce ones; nextest still runs every binary at once. The hardware
# tests take turns through the `hardware` test group in
# `.config/nextest.toml`, which is what keeps the `macos-latest` VideoToolbox
# VP9 decoder from failing a decode while another session decodes (#565).
# These were dealt out over shards before (#595); they are not split now.
native-tests:
name: Native tests (${{ matrix.os }})
needs: changes
if: needs.changes.outputs.native_tests == 'true'
strategy:
fail-fast: false
matrix:
os:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
run: rustup show
# This job rebuilt every dependency from a cold `target/` on every run
# before this step existed (#352). The cache is keyed on the job, the
# runner, the toolchain and the manifest, so the `[profile.bench]`
# fat-LTO artifacts and the `[profile.dev]` ones never share an entry;
# `cache-on-failure` keeps a failed run's dependency build, since the
# dependencies are not what failed.
- name: Cache cargo registry and build artifacts
uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
# The `cfg(windows)` half of the tree - `zvidlib-hardware`'s
# `windows_mf.rs` and `windows_mf_encoder.rs`, the Windows arm of its
# `nvdec.rs`, and the `windows` crate feature list - only ever compiles
# here, so linting it anywhere else is impossible. Same `-D warnings`
# gate the Linux job uses, so a platform-specific warning fails the build
# rather than waiting for someone to build on that platform by hand.
# Clippy type-checks every target as it goes, which is why there is no
# `cargo check` before it.
- name: Lint native target
run: cargo clippy --workspace --all-targets --features native -- -D warnings
- name: Install cargo-nextest
uses: taiki-e/install-action@v2
with:
tool: nextest
- name: Run the tests of the packages this run selected
shell: bash
env:
PACKAGES: ${{ needs.changes.outputs.packages }}
run: |
packages=()
targets() {
if [ "$PACKAGES" = all ] || jq -e --arg p "$1" 'index($p) != null' <<<"$PACKAGES" >/dev/null; then
packages+=(-p "$1")
fi
}
targets zvidlib
targets zvidlib-aac-encoder
targets zvidlib-av1
targets zvidlib-av1-decoder
targets zvidlib-av1-encoder
targets zvidlib-av1-syntax
targets zvidlib-bench-support
targets zvidlib-build
targets zvidlib-color
targets zvidlib-container
targets zvidlib-core
targets zvidlib-hardware
targets zvidlib-hevc
targets zvidlib-hevc-decoder
targets zvidlib-hevc-encoder
targets zvidlib-hevc-syntax
targets zvidlib-opus
targets zvidlib-opus-syntax
targets zvidlib-vorbis-decoder
targets zvidlib-vorbis-encoder
targets zvidlib-vorbis-syntax
targets zvidlib-vp8
targets zvidlib-vp9-decoder
targets zvidlib-vp9-encoder
targets zvidlib-vp9-syntax
cargo nextest run --profile ci --no-tests=warn --features native \
--lib --tests --examples "${packages[@]}"
# `--release` is the profile #327 was reported against: nothing about the
# missing rpath is profile-specific, but this is the invocation the issue
# names and the one the Intel benchmark column is measured with, so it is the
# one worth asserting launches unaided. `cargo build` first so the shipped
# cdylib exists for the rpath test to check as well - `cargo test` alone links
# the library as an rlib and never produces it. Neither step sets a `DYLD_*`
# variable, which is the point.
#
# Only the rpath test needs the release profile. This step also used to rerun
# every library unit in release, which was most of its four and a half
# minutes and repeated what the test jobs run (#595); it is its own job now
# so the fat-LTO build is off the test jobs' path entirely.
macos-swift-rpath:
name: macOS Swift runtime rpath
needs: changes
if: needs.changes.outputs.macos_swift_rpath == 'true'
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
run: rustup show
- name: Cache cargo registry and build artifacts
uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: Test the macOS Swift runtime rpath
run: |
cargo build --release --features native
cargo test --release --features native --test macos_swift_runtime_rpath -- --nocapture