refactor: keep one full record per line

Each line of measurements/<emulator>.jsonl now holds the whole run:
every repetition, core logs, per-frame hashes and raw timings. The gzip
data files, the data schema and the split and join code are gone, and
core_probe --record appends one line. record_version moves to 3.

The seven records are rewritten once: each new line equals its old line
joined with its data file, and rebuilds the first-layout file its id
hashes.
This commit is contained in:
Abdessamad Derraz committed 2026-10-27 09:57:39 +01:00
1 parent 9c34e8891f
commit da68f19411
21 files changed
+334 -659

No files matched your search

+1 -3
View File
@@ -23,7 +23,5 @@ bios/** -text
data/** -text
# Measurement records are append-only: two branches that each add a line to
# the same file merge by keeping both. A data file is bytes its line vouches
# for by sha256.
# the same file merge by keeping both.
measurements/*.jsonl text eol=lf merge=union
measurements/data/** -text
+6 -6
View File
@@ -1,6 +1,6 @@
{"check":{"claim":"The mpeg_card.bin image Beetle Saturn loads changes what the Saturn BIOS shows and does for a Video CD.","method":"Saturn BIOS v1.01 (Japan), region forced to Japan, Video CD Card option enabled, automatic RTC left on and the host clock frozen at 2000-01-01 00:00:00 by libfaketime 0.9.10 so runs are deterministic, the authored Video CD inserted. 3000 frames; Saturn A (libretro B) held 15 frames from frame 1300, which is Play on the BIOS CD player, and Start 15 frames from frame 1900. Repeated with each mpeg_card.bin image, with no file, with 512 KiB of zeros and with a 1-byte file; every other input is identical. Each variant runs twice in fresh processes.","observable":"video_sha1","result":{"groups":[{"value":"8fe992a692603f9e0e4cefacc1a8348d17409463","variants":["mpeg_card.bin","mpeg_card.bin.3fee63d4","mpeg_card.bin.80179c49","mpeg_card.bin.821af2bd","mpeg_card.bin.b60e8866","mpeg_card.bin.b7450812","mpeg_card.bin.ee821d3f","mpeg_card.bin.ef9de845","mpeg_card.bin-absent","zeros_512k.bin","one_byte.bin"]}],"statement":"video_sha1 is identical across all 11 variants","status":"same"},"variable":"mpeg_card.bin"},"core":{"build_command":"make -j8","commit":"2ce77e20a956dc9a67b8d3d527f477b984310feb","compiler":["GCC: (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0"],"file":"mednafen_saturn_libretro.so","library_name":"Beetle Saturn","library_version":"v1.32.1","repo":"https://github.com/libretro/beetle-saturn-libretro","sha256":"6e9a44698a2eee492e8d7eff67f2bd5634f52de8cc1af7885f334c3f29fcf2bd"},"data":{"bytes":4926,"sha256":"04a64ac3986e57626465e6a946a52fc52cccaa35b0bae7e97f7051f5286af1ca"},"emulator":"beetle_saturn","flags":["load average reached 7.02 (threshold 6.0)"],"frontend":{"commit":"8b12685c1652732828c146336a14a5fc265fffd3","script":"scripts/core_probe.py","script_modified":false,"script_sha256":"a95ab54a8d8f9ecc5dc2a55fd0ab93ffc66fe6466e2d89eea56a1a95c17d858b"},"host":{"cpu_count":12,"cpu_model":"13th Gen Intel(R) Core(TM) i9-13900K","kernel":"6.18.54.1-microsoft-standard-WSL2","load_after":[7.02,7.37,5.72],"load_before":[3.87,7.46,5.31],"machine":"x86_64","max_load":6.0,"os":"Linux-6.18.54.1-microsoft-standard-WSL2-x86_64-with-glibc2.39","python":"3.12.3"},"id":"2026-10-27-6afcf7b700ba","inputs":{"content":{"label":"authored Video CD: Big Buck Bunny 60 s, NTSC VCD 2.0 (identity.json beside the image)","sha1":"150cfb81d3c844f0721f2864073a33e326492d46","size":172,"tracks":[{"file":"bbb_vcd.bin","sha1":"e7952e60928dba7f3ceb6c3316894f96ad24e175","size":12169248}]},"env":{"FAKETIME":"2000-01-01 00:00:00","FAKETIME_DONT_FAKE_MONOTONIC":"1","LD_PRELOAD":"libfaketime.so.1 sha256:7ac8a1cdff1b627548de11513fddedd75014f5b041e9ae9054a634bb04aa7982"},"frame_hash_every":150,"frames":3000,"joypad_ports":2,"observables":["frame_sha1","video_sha1","audio_sha1","state_sha1","state:MPEG.AuthState","state:MPEG.Present","frame_hashes"],"options":{"beetle_saturn_mpeg_card":"enabled","beetle_saturn_region":"Japan"},"presses":[{"button":0,"frame":1300,"length":15},{"button":3,"frame":1900,"length":15}],"repetitions":2,"warmup":0},"kind":"check","migrated_from_version":1,"record_version":2,"recorded_at":"2026-10-27T04:57:35+00:00","subject":"mpeg-card-vcd-bios-jp","supersedes":null,"variants":[{"files_opened":[{"path":"sega_101.bin","sha1_after":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720","sha1_staged":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"},{"path":"mpeg_card.bin","sha1_after":"2f5ebd434f6d5b82e93d03e5626007bd5380f652","sha1_staged":"2f5ebd434f6d5b82e93d03e5626007bd5380f652"}],"files_probed_absent":[],"flags":[],"label":"mpeg_card.bin","loaded":true,"observables":{"audio_sha1":"ebe47d681c9a51cfaa5c9228920e0b7b2c494139","frame_sha1":"8f9c76e0c859397ce327af31c86b11c756042151","state:MPEG.AuthState":"00","state:MPEG.Present":"01","state_sha1":"5d655c084dfb54731bd2683a3fe3006f7ceb8d5a","video_sha1":"8fe992a692603f9e0e4cefacc1a8348d17409463"},"repetitions":2,"system_files":{"mpeg_card.bin":"2f5ebd434f6d5b82e93d03e5626007bd5380f652","sega_101.bin":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"},"tracing":"strace"},{"files_opened":[{"path":"sega_101.bin","sha1_after":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720","sha1_staged":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"},{"path":"mpeg_card.bin","sha1_after":"eaac576b8b5e79e7df5e3efd70275b220ff70d78","sha1_staged":"eaac576b8b5e79e7df5e3efd70275b220ff70d78"}],"files_probed_absent":[],"flags":[],"label":"mpeg_card.bin.3fee63d4","loaded":true,"observables":{"audio_sha1":"ebe47d681c9a51cfaa5c9228920e0b7b2c494139","frame_sha1":"8f9c76e0c859397ce327af31c86b11c756042151","state:MPEG.AuthState":"00","state:MPEG.Present":"01","state_sha1":"5d655c084dfb54731bd2683a3fe3006f7ceb8d5a","video_sha1":"8fe992a692603f9e0e4cefacc1a8348d17409463"},"repetitions":2,"system_files":{"mpeg_card.bin":"eaac576b8b5e79e7df5e3efd70275b220ff70d78","sega_101.bin":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"},"tracing":"strace"},{"files_opened":[{"path":"sega_101.bin","sha1_after":"df94c5b4d47eb3cc404d88b33aLine truncated
{"check":{"claim":"The mpeg_card.bin image Beetle Saturn loads changes what the Saturn BIOS shows and does for a Video CD.","method":"Saturn BIOS v1.00a (mpr-17933), region forced to North America, Video CD Card option enabled, automatic RTC left on and the host clock frozen at 2000-01-01 00:00:00 by libfaketime 0.9.10 so runs are deterministic, the authored Video CD inserted. 3000 frames; Saturn A (libretro B) held 15 frames from frame 1300, which is Play on the BIOS CD player, and Start 15 frames from frame 1900. Repeated with each mpeg_card.bin image, with no file, with 512 KiB of zeros and with a 1-byte file; every other input is identical. Each variant runs twice in fresh processes.","observable":"video_sha1","result":{"groups":[{"value":"6f95024fbc87ea46a22be37e04dabe558b900601","variants":["mpeg_card.bin","mpeg_card.bin.3fee63d4","mpeg_card.bin.80179c49","mpeg_card.bin.821af2bd","mpeg_card.bin.b60e8866","mpeg_card.bin.b7450812","mpeg_card.bin.ee821d3f","mpeg_card.bin.ef9de845","mpeg_card.bin-absent","zeros_512k.bin","one_byte.bin"]}],"statement":"video_sha1 is identical across all 11 variants","status":"same"},"variable":"mpeg_card.bin"},"core":{"build_command":"make -j8","commit":"2ce77e20a956dc9a67b8d3d527f477b984310feb","compiler":["GCC: (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0"],"file":"mednafen_saturn_libretro.so","library_name":"Beetle Saturn","library_version":"v1.32.1","repo":"https://github.com/libretro/beetle-saturn-libretro","sha256":"6e9a44698a2eee492e8d7eff67f2bd5634f52de8cc1af7885f334c3f29fcf2bd"},"data":{"bytes":4938,"sha256":"dd4067496e52ae38b7a3c0422fc11bb3a0fb0183d72d31a2f29395f2c423a789"},"emulator":"beetle_saturn","flags":["load average reached 8.61 (threshold 6.0)"],"frontend":{"commit":"8b12685c1652732828c146336a14a5fc265fffd3","script":"scripts/core_probe.py","script_modified":false,"script_sha256":"a95ab54a8d8f9ecc5dc2a55fd0ab93ffc66fe6466e2d89eea56a1a95c17d858b"},"host":{"cpu_count":12,"cpu_model":"13th Gen Intel(R) Core(TM) i9-13900K","kernel":"6.18.54.1-microsoft-standard-WSL2","load_after":[8.61,7.5,6.41],"load_before":[3.72,6.03,5.7],"machine":"x86_64","max_load":6.0,"os":"Linux-6.18.54.1-microsoft-standard-WSL2-x86_64-with-glibc2.39","python":"3.12.3"},"id":"2026-10-27-ac66124f74ae","inputs":{"content":{"label":"authored Video CD: Big Buck Bunny 60 s, NTSC VCD 2.0 (identity.json beside the image)","sha1":"150cfb81d3c844f0721f2864073a33e326492d46","size":172,"tracks":[{"file":"bbb_vcd.bin","sha1":"e7952e60928dba7f3ceb6c3316894f96ad24e175","size":12169248}]},"env":{"FAKETIME":"2000-01-01 00:00:00","FAKETIME_DONT_FAKE_MONOTONIC":"1","LD_PRELOAD":"libfaketime.so.1 sha256:7ac8a1cdff1b627548de11513fddedd75014f5b041e9ae9054a634bb04aa7982"},"frame_hash_every":150,"frames":3000,"joypad_ports":2,"observables":["frame_sha1","video_sha1","audio_sha1","state_sha1","state:MPEG.AuthState","state:MPEG.Present","frame_hashes"],"options":{"beetle_saturn_mpeg_card":"enabled","beetle_saturn_region":"North America"},"presses":[{"button":0,"frame":1300,"length":15},{"button":3,"frame":1900,"length":15}],"repetitions":2,"warmup":0},"kind":"check","migrated_from_version":1,"record_version":2,"recorded_at":"2026-10-27T05:07:13+00:00","subject":"mpeg-card-vcd-bios-us","supersedes":null,"variants":[{"files_opened":[{"path":"mpr-17933.bin","sha1_after":"faa8ea183a6d7bbe5d4e03bb1332519800d3fbc3","sha1_staged":"faa8ea183a6d7bbe5d4e03bb1332519800d3fbc3"},{"path":"mpeg_card.bin","sha1_after":"2f5ebd434f6d5b82e93d03e5626007bd5380f652","sha1_staged":"2f5ebd434f6d5b82e93d03e5626007bd5380f652"}],"files_probed_absent":[],"flags":[],"label":"mpeg_card.bin","loaded":true,"observables":{"audio_sha1":"9c8887d4ee16309b6da657ede701a90a97f1e61a","frame_sha1":"11954eea7c17ac7e0d3b46bf24047b657601b37c","state:MPEG.AuthState":"00","state:MPEG.Present":"01","state_sha1":"f042988b6fbe3f487d0bb370441645563f2add47","video_sha1":"6f95024fbc87ea46a22be37e04dabe558b900601"},"repetitions":2,"system_files":{"mpeg_card.bin":"2f5ebd434f6d5b82e93d03e5626007bd5380f652","mpr-17933.bin":"faa8ea183a6d7bbe5d4e03bb1332519800d3fbc3"},"tracing":"strace"},{"files_opened":[{"path":"mpr-17933.bin","sha1_after":"faa8ea183a6d7bbe5d4e03bb1332519800d3fbc3","sha1_staged":"faa8ea183a6d7bbe5d4e03bb1332519800d3fbc3"},{"path":"mpeg_card.bin","sha1_after":"eaac576b8b5e79e7df5e3efd70275b220ff70d78","sha1_staged":"eaac576b8b5e79e7df5e3efd70275b220ff70d78"}],"files_probed_absent":[],"flags":[],"label":"mpeg_card.bin.3fee63d4","loaded":true,"observables":{"audio_sha1":"9c8887d4ee16309b6da657ede701a90a97f1e61a","frame_sha1":"11954eea7c17ac7e0d3b46bf24047b657601b37c","state:MPEG.AuthState":"00","state:MPEG.Present":"01","state_sha1":"f042988b6fbe3f487d0bb370441645563f2add47","video_sha1":"6f95024fbc87ea46a22be37e04dabe558b900601"},"repetitions":2,"system_files":{"mpeg_card.bin":"eaac576b8b5e79e7df5e3efd70275b220ff70d78","mpr-17933.bin":"faa8ea183a6d7bbe5d4e03bb1332519800d3fbc3"},"tracing":"strace"},{"files_opened":[{"path":"mpr-17933.bin","sha1_after":"faLine truncated
{"check":{"claim":"The mpeg_card.bin image Beetle Saturn loads changes what the Saturn BIOS shows and does for a Video CD.","method":"Experimental build, not what users run: mednafen/ss/cdb.c reads the AUTH_DEVICE and GET_AUTH device type from CR2 (CTR.CD[1]) instead of CR3 (CTR.CD[2]), plus a command trace to a file. Saturn BIOS v1.01 (Japan), region forced to Japan, Video CD Card option enabled, automatic RTC left on and the host clock frozen at 2000-01-01 00:00:00 by libfaketime 0.9.10 so runs are deterministic, the authored Video CD inserted. 3000 frames; Saturn A (libretro B) held 15 frames from frame 1300, which is Play on the BIOS CD player, and Start 15 frames from frame 1900. Repeated with each mpeg_card.bin image, with no file, with 512 KiB of zeros and with a 1-byte file; every other input is identical. Each variant runs twice in fresh processes.","observable":"video_sha1","result":{"groups":[{"value":"88683d404f1266d0177c73f970cbd5996c11b7c1","variants":["mpeg_card.bin","mpeg_card.bin.3fee63d4","mpeg_card.bin.80179c49","mpeg_card.bin.821af2bd","mpeg_card.bin.b60e8866","mpeg_card.bin.b7450812","mpeg_card.bin.ee821d3f","mpeg_card.bin.ef9de845","zeros_512k.bin","one_byte.bin"]},{"value":"4283d78ac91cfef738daf1fe682dff4e531809e4","variants":["mpeg_card.bin-absent"]}],"statement":"video_sha1 takes 2 distinct values across 11 variants","status":"differs"},"variable":"mpeg_card.bin"},"core":{"build_command":"make -j8 after editing mednafen/ss/cdb.c: AUTH_DEVICE and GET_AUTH read CTR.CD[1] instead of CTR.CD[2], plus a command trace","commit":"2ce77e20a956dc9a67b8d3d527f477b984310feb","compiler":["GCC: (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0"],"file":"mednafen_saturn_libretro.so","library_name":"Beetle Saturn","library_version":"v1.32.1","repo":"https://github.com/libretro/beetle-saturn-libretro","sha256":"fc9c33c697ddcde7547f2315f6ff91231f2ecfa24280c884a0185b880ffc2dfb"},"data":{"bytes":5511,"sha256":"acc628a32b9d6e56d4e56892741c15f41c8d8affe743caf8bf684d4b59109673"},"emulator":"beetle_saturn","flags":[],"frontend":{"commit":"8b12685c1652732828c146336a14a5fc265fffd3","script":"scripts/core_probe.py","script_modified":false,"script_sha256":"a95ab54a8d8f9ecc5dc2a55fd0ab93ffc66fe6466e2d89eea56a1a95c17d858b"},"host":{"cpu_count":12,"cpu_model":"13th Gen Intel(R) Core(TM) i9-13900K","kernel":"6.18.54.1-microsoft-standard-WSL2","load_after":[2.41,4.58,5.69],"load_before":[3.24,7.0,6.58],"machine":"x86_64","max_load":6.0,"os":"Linux-6.18.54.1-microsoft-standard-WSL2-x86_64-with-glibc2.39","python":"3.12.3"},"id":"2026-10-27-43527b763ace","inputs":{"content":{"label":"authored Video CD: Big Buck Bunny 60 s, NTSC VCD 2.0 (identity.json beside the image)","sha1":"150cfb81d3c844f0721f2864073a33e326492d46","size":172,"tracks":[{"file":"bbb_vcd.bin","sha1":"e7952e60928dba7f3ceb6c3316894f96ad24e175","size":12169248}]},"env":{"FAKETIME":"2000-01-01 00:00:00","FAKETIME_DONT_FAKE_MONOTONIC":"1","LD_PRELOAD":"libfaketime.so.1 sha256:7ac8a1cdff1b627548de11513fddedd75014f5b041e9ae9054a634bb04aa7982"},"frame_hash_every":150,"frames":3000,"joypad_ports":2,"observables":["frame_sha1","video_sha1","audio_sha1","state_sha1","state:MPEG.AuthState","state:MPEG.Present","frame_hashes"],"options":{"beetle_saturn_mpeg_card":"enabled","beetle_saturn_region":"Japan"},"presses":[{"button":0,"frame":1300,"length":15},{"button":3,"frame":1900,"length":15}],"repetitions":2,"warmup":0},"kind":"check","migrated_from_version":1,"record_version":2,"recorded_at":"2026-10-27T05:14:32+00:00","subject":"mpeg-card-vcd-bios-jp-cr2-build","supersedes":null,"variants":[{"files_opened":[{"path":"sega_101.bin","sha1_after":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720","sha1_staged":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"},{"path":"mpeg_card.bin","sha1_after":"2f5ebd434f6d5b82e93d03e5626007bd5380f652","sha1_staged":"2f5ebd434f6d5b82e93d03e5626007bd5380f652"}],"files_probed_absent":[],"flags":[],"label":"mpeg_card.bin","loaded":true,"observables":{"audio_sha1":"85f24b268528b103056d511f60380a0926a71a22","frame_sha1":"e0225061de9829e563617fd30ae763af6d8c6394","state:MPEG.AuthState":"02","state:MPEG.Present":"01","state_sha1":"96e879086e2652dece66319ca3e40519cec3ea26","video_sha1":"88683d404f1266d0177c73f970cbd5996c11b7c1"},"repetitions":2,"system_files":{"mpeg_card.bin":"2f5ebd434f6d5b82e93d03e5626007bd5380f652","sega_101.bin":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"},"tracing":"strace"},{"files_opened":[{"path":"sega_101.bin","sha1_after":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720","sha1_staged":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"},{"path":"mpeg_card.bin","sha1_after":"eaac576b8b5e79e7df5e3efd70275b220ff70d78","sha1_staged":"eaac576b8b5e79e7df5e3efd70275b220ff70d78"}],"files_probed_absent":[],"flags":[],"label":"mpeg_card.bin.3fee63d4","loaded":true,"observables":{"audio_sha1":"85f24b268528b103056d511f60380a0926a71a22","frame_sha1":"e0225061de9829e563617fd30ae763af6d8c6394","state:MPEG.AuthState":"02","state:MPEG.Present":"01","state_sha1":"96e87Line truncated
{"check":{"claim":"The mpeg_card.bin image Beetle Saturn loads changes what the Saturn BIOS shows and does for a Video CD.","method":"Saturn BIOS v1.01 (Japan), region forced to Japan, Video CD Card option enabled, automatic RTC left on and the host clock running (no libfaketime), to test whether runs repeat, the authored Video CD inserted. 3000 frames; Saturn A (libretro B) held 15 frames from frame 1300, which is Play on the BIOS CD player, and Start 15 frames from frame 1900. Repeated with each mpeg_card.bin image, with no file, with 512 KiB of zeros and with a 1-byte file; every other input is identical. Each variant runs twice in fresh processes.","observable":"video_sha1","result":{"groups":[{"value":"bde95c486614dc797ba5a939fb738d947b479fa8","variants":["mpeg_card.bin"]},{"value":"5a14993408a486a4c55341ca7284f89e148dfc18","variants":["mpeg_card.bin.3fee63d4"]},{"value":"7e3d403552c31c494beb9847262bf86cf266c3fa","variants":["mpeg_card.bin.80179c49"]},{"value":"b639d8c3130638876b398c66f463b98f535a2497","variants":["mpeg_card.bin.821af2bd"]},{"value":"5dd8db90027d8ef7670c0805751b8973920195a9","variants":["mpeg_card.bin.b60e8866"]},{"value":"fabe749bf34c09de4c26d4328e2744876488a18e","variants":["mpeg_card.bin.b7450812"]},{"value":"ef9d0fab7815222344d7d669bd0dbc6e0d811174","variants":["mpeg_card.bin.ee821d3f"]},{"value":"164f2e824cc15672226bebc07998f40d5e3422ce","variants":["mpeg_card.bin.ef9de845"]},{"value":"1c920f21c6fc3d402fa45a7a5fb4d302a82d4898","variants":["mpeg_card.bin-absent"]},{"value":"2a6d6e171b0bafe5b60ec41a37db4322ff2b12e8","variants":["zeros_512k.bin"]},{"value":"2d5be7af25846153e20081a8b12b614ff348c4b9","variants":["one_byte.bin"]}],"statement":"video_sha1 is not settled in 11 of 11 variants (mpeg_card.bin, mpeg_card.bin.3fee63d4, mpeg_card.bin.80179c49, mpeg_card.bin.821af2bd, mpeg_card.bin.b60e8866, mpeg_card.bin.b7450812, mpeg_card.bin.ee821d3f, mpeg_card.bin.ef9de845, mpeg_card.bin-absent, zeros_512k.bin, one_byte.bin): no comparison is drawn","status":"inconclusive"},"variable":"mpeg_card.bin"},"core":{"build_command":"make -j8","commit":"2ce77e20a956dc9a67b8d3d527f477b984310feb","compiler":["GCC: (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0"],"file":"mednafen_saturn_libretro.so","library_name":"Beetle Saturn","library_version":"v1.32.1","repo":"https://github.com/libretro/beetle-saturn-libretro","sha256":"6e9a44698a2eee492e8d7eff67f2bd5634f52de8cc1af7885f334c3f29fcf2bd"},"data":{"bytes":13793,"sha256":"c7d8f312e87a1d96222529cef96fb84341ea3948694106b4e4e79d224666ef4e"},"emulator":"beetle_saturn","flags":["mpeg_card.bin: repetition 1 disagrees with repetition 0 on frame_hashes, frame_sha1, state_sha1, video_sha1","mpeg_card.bin.3fee63d4: repetition 1 disagrees with repetition 0 on frame_hashes, frame_sha1, state_sha1, video_sha1","mpeg_card.bin.80179c49: repetition 1 disagrees with repetition 0 on frame_hashes, frame_sha1, state_sha1, video_sha1","mpeg_card.bin.821af2bd: repetition 1 disagrees with repetition 0 on frame_hashes, frame_sha1, state_sha1, video_sha1","mpeg_card.bin.b60e8866: repetition 1 disagrees with repetition 0 on frame_hashes, frame_sha1, state_sha1, video_sha1","mpeg_card.bin.b7450812: repetition 1 disagrees with repetition 0 on frame_hashes, frame_sha1, state_sha1, video_sha1","mpeg_card.bin.ee821d3f: repetition 1 disagrees with repetition 0 on frame_hashes, frame_sha1, state_sha1, video_sha1","mpeg_card.bin.ef9de845: repetition 1 disagrees with repetition 0 on frame_hashes, frame_sha1, state_sha1, video_sha1","mpeg_card.bin-absent: repetition 1 disagrees with repetition 0 on frame_hashes, frame_sha1, state_sha1, video_sha1","zeros_512k.bin: repetition 1 disagrees with repetition 0 on frame_hashes, frame_sha1, state_sha1, video_sha1","one_byte.bin: repetition 1 disagrees with repetition 0 on frame_hashes, frame_sha1, state_sha1, video_sha1"],"frontend":{"commit":"8b12685c1652732828c146336a14a5fc265fffd3","script":"scripts/core_probe.py","script_modified":false,"script_sha256":"a95ab54a8d8f9ecc5dc2a55fd0ab93ffc66fe6466e2d89eea56a1a95c17d858b"},"host":{"cpu_count":12,"cpu_model":"13th Gen Intel(R) Core(TM) i9-13900K","kernel":"6.18.54.1-microsoft-standard-WSL2","load_after":[1.77,2.58,4.32],"load_before":[1.81,3.2,4.89],"machine":"x86_64","max_load":6.0,"os":"Linux-6.18.54.1-microsoft-standard-WSL2-x86_64-with-glibc2.39","python":"3.12.3"},"id":"2026-10-27-bf3d8283b265","inputs":{"content":{"label":"authored Video CD: Big Buck Bunny 60 s, NTSC VCD 2.0 (identity.json beside the image)","sha1":"150cfb81d3c844f0721f2864073a33e326492d46","size":172,"tracks":[{"file":"bbb_vcd.bin","sha1":"e7952e60928dba7f3ceb6c3316894f96ad24e175","size":12169248}]},"env":{},"frame_hash_every":150,"frames":3000,"joypad_ports":2,"observables":["frame_sha1","video_sha1","audio_sha1","state_sha1","state:MPEG.AuthState","state:MPEG.Present","frame_hashes"],"options":{"beetle_saturn_mpeg_card":"enabled","beetle_saturn_region":"Japan"},"presses":[{"button":0,"frame":1300,"length":15},{"button":3,"frLine truncated
{"check":{"claim":"The mpeg_card.bin image Beetle Saturn loads decides whether Lunar - Silver Star Story MPEG (T-27904G) detects the Video CD Card and plays its movies.","method":"Saturn BIOS v1.01 (Japan), Video CD Card option enabled, region auto-detected from the disc (Japan), host clock frozen at 2000-01-01 00:00:00 by libfaketime 0.9.10 so runs are deterministic. 4800 frames; Start held 15 frames at frames 2400, 3000, 3600 and 4200. Repeated with each mpeg_card.bin image, with no file, with 512 KiB of zeros and with a 1-byte file; every other input is identical. Each variant runs twice in fresh processes.","observable":"video_sha1","result":{"groups":[{"value":"e248465e55f8edbdfb16a8ca6588817ffa9d8064","variants":["mpeg_card.bin","mpeg_card.bin.3fee63d4","mpeg_card.bin.80179c49","mpeg_card.bin.821af2bd","mpeg_card.bin.b60e8866","mpeg_card.bin.b7450812","mpeg_card.bin.ee821d3f","mpeg_card.bin.ef9de845","mpeg_card.bin-absent","zeros_512k.bin","one_byte.bin"]}],"statement":"video_sha1 is identical across all 11 variants","status":"same"},"variable":"mpeg_card.bin"},"core":{"build_command":"make -j8","commit":"2ce77e20a956dc9a67b8d3d527f477b984310feb","compiler":["GCC: (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0"],"file":"mednafen_saturn_libretro.so","library_name":"Beetle Saturn","library_version":"v1.32.1","repo":"https://github.com/libretro/beetle-saturn-libretro","sha256":"6e9a44698a2eee492e8d7eff67f2bd5634f52de8cc1af7885f334c3f29fcf2bd"},"data":{"bytes":4612,"sha256":"77a9947ab6a667b4bbc87fa202c599bdea51815c2ad615512fc347ffded82b20"},"emulator":"beetle_saturn","flags":[],"frontend":{"commit":"8b12685c1652732828c146336a14a5fc265fffd3","script":"scripts/core_probe.py","script_modified":false,"script_sha256":"a95ab54a8d8f9ecc5dc2a55fd0ab93ffc66fe6466e2d89eea56a1a95c17d858b"},"host":{"cpu_count":12,"cpu_model":"13th Gen Intel(R) Core(TM) i9-13900K","kernel":"6.18.54.1-microsoft-standard-WSL2","load_after":[3.16,2.62,3.83],"load_before":[1.77,2.58,4.32],"machine":"x86_64","max_load":6.0,"os":"Linux-6.18.54.1-microsoft-standard-WSL2-x86_64-with-glibc2.39","python":"3.12.3"},"id":"2026-10-27-5d8f0b375830","inputs":{"content":{"label":"Redump: Lunar - Silver Star Story Complete (Japan), T-27904G V1.00 (cue sha1 7a609a9b579de648197ccfb5ab7bf4b3beb07d95)","sha1":"7a609a9b579de648197ccfb5ab7bf4b3beb07d95","size":554,"tracks":[{"file":"Lunar - Silver Star Story Complete (Japan) (Track 1).bin","sha1":"b6d71e1c96211af46d83278eccf7d6827a57d004","size":83710032},{"file":"Lunar - Silver Star Story Complete (Japan) (Track 2).bin","sha1":"c2406d2953cec7f9fe5826aa71c7359c2baeb3a7","size":491934912},{"file":"Lunar - Silver Star Story Complete (Japan) (Track 3).bin","sha1":"71c3bcfc22038d0f25126b34f703aa464259cfb5","size":25867296},{"file":"Lunar - Silver Star Story Complete (Japan) (Track 4).bin","sha1":"b9c73d3890cd699cb1718131c2f7f04897a14d65","size":1872192}]},"env":{"FAKETIME":"2000-01-01 00:00:00","FAKETIME_DONT_FAKE_MONOTONIC":"1","LD_PRELOAD":"libfaketime.so.1 sha256:7ac8a1cdff1b627548de11513fddedd75014f5b041e9ae9054a634bb04aa7982"},"frame_hash_every":300,"frames":4800,"joypad_ports":2,"observables":["frame_sha1","video_sha1","audio_sha1","state_sha1","state:MPEG.AuthState","state:MPEG.Present","frame_hashes"],"options":{"beetle_saturn_mpeg_card":"enabled"},"presses":[{"button":3,"frame":2400,"length":15},{"button":3,"frame":3000,"length":15},{"button":3,"frame":3600,"length":15},{"button":3,"frame":4200,"length":15}],"repetitions":2,"warmup":0},"kind":"check","migrated_from_version":1,"record_version":2,"recorded_at":"2026-10-27T05:26:27+00:00","subject":"mpeg-card-lunar","supersedes":null,"variants":[{"files_opened":[{"path":"sega_101.bin","sha1_after":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720","sha1_staged":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"},{"path":"mpeg_card.bin","sha1_after":"2f5ebd434f6d5b82e93d03e5626007bd5380f652","sha1_staged":"2f5ebd434f6d5b82e93d03e5626007bd5380f652"}],"files_probed_absent":[],"flags":[],"label":"mpeg_card.bin","loaded":true,"observables":{"audio_sha1":"6e7c9654e83f3735ee1adbf235d1f84fe62f7a64","frame_sha1":"fae70b43cd89dcdf806d0ce9627f5b969c43677a","state:MPEG.AuthState":"00","state:MPEG.Present":"01","state_sha1":"9397ac4deb13ddcee4183c052ef48811e82f4c8f","video_sha1":"e248465e55f8edbdfb16a8ca6588817ffa9d8064"},"repetitions":2,"system_files":{"mpeg_card.bin":"2f5ebd434f6d5b82e93d03e5626007bd5380f652","sega_101.bin":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"},"tracing":"strace"},{"files_opened":[{"path":"sega_101.bin","sha1_after":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720","sha1_staged":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"},{"path":"mpeg_card.bin","sha1_after":"eaac576b8b5e79e7df5e3efd70275b220ff70d78","sha1_staged":"eaac576b8b5e79e7df5e3efd70275b220ff70d78"}],"files_probed_absent":[],"flags":[],"label":"mpeg_card.bin.3fee63d4","loaded":true,"observables":{"audio_sha1":"6e7c9654e83f3735ee1adbf235d1f84fe62f7a64","frame_sha1":"fae70b43cd89dcdf806d0ce9627f5b969c43677a","state:Line truncated
{"check":{"claim":"The mpeg_card.bin image Beetle Saturn loads decides whether Lunar - Silver Star Story MPEG (T-27904G) detects the Video CD Card and plays its movies.","method":"Experimental build, not what users run: AUTH_DEVICE and GET_AUTH read the device type from CR2. Saturn BIOS v1.01 (Japan), Video CD Card option enabled, region auto-detected from the disc (Japan), host clock frozen at 2000-01-01 00:00:00 by libfaketime 0.9.10 so runs are deterministic. 4800 frames; Start held 15 frames at frames 2400, 3000, 3600 and 4200. Repeated with each mpeg_card.bin image, with no file, with 512 KiB of zeros and with a 1-byte file; every other input is identical. Each variant runs twice in fresh processes.","observable":"video_sha1","result":{"groups":[{"value":"f7db10347ce2081c188f83fbe5640131b4f2ceb5","variants":["mpeg_card.bin","mpeg_card.bin.3fee63d4","mpeg_card.bin.80179c49","mpeg_card.bin.821af2bd","mpeg_card.bin.b60e8866","mpeg_card.bin.b7450812","mpeg_card.bin.ee821d3f","mpeg_card.bin.ef9de845","zeros_512k.bin","one_byte.bin"]},{"value":"d60fcae489df0c317cca4e95f6f57c323be20ee4","variants":["mpeg_card.bin-absent"]}],"statement":"video_sha1 takes 2 distinct values across 11 variants","status":"differs"},"variable":"mpeg_card.bin"},"core":{"build_command":"make -j8 after editing mednafen/ss/cdb.c: AUTH_DEVICE and GET_AUTH read CTR.CD[1] instead of CTR.CD[2], plus a command trace","commit":"2ce77e20a956dc9a67b8d3d527f477b984310feb","compiler":["GCC: (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0"],"file":"mednafen_saturn_libretro.so","library_name":"Beetle Saturn","library_version":"v1.32.1","repo":"https://github.com/libretro/beetle-saturn-libretro","sha256":"fc9c33c697ddcde7547f2315f6ff91231f2ecfa24280c884a0185b880ffc2dfb"},"data":{"bytes":4857,"sha256":"0eae6e67292591c72fdbb0f4d2cf97f48b6738971217c1c80c74225576124da8"},"emulator":"beetle_saturn","flags":[],"frontend":{"commit":"8b12685c1652732828c146336a14a5fc265fffd3","script":"scripts/core_probe.py","script_modified":false,"script_sha256":"a95ab54a8d8f9ecc5dc2a55fd0ab93ffc66fe6466e2d89eea56a1a95c17d858b"},"host":{"cpu_count":12,"cpu_model":"13th Gen Intel(R) Core(TM) i9-13900K","kernel":"6.18.54.1-microsoft-standard-WSL2","load_after":[3.24,2.85,3.56],"load_before":[3.16,2.62,3.83],"machine":"x86_64","max_load":6.0,"os":"Linux-6.18.54.1-microsoft-standard-WSL2-x86_64-with-glibc2.39","python":"3.12.3"},"id":"2026-10-27-f532e5006714","inputs":{"content":{"label":"Redump: Lunar - Silver Star Story Complete (Japan), T-27904G V1.00 (cue sha1 7a609a9b579de648197ccfb5ab7bf4b3beb07d95)","sha1":"7a609a9b579de648197ccfb5ab7bf4b3beb07d95","size":554,"tracks":[{"file":"Lunar - Silver Star Story Complete (Japan) (Track 1).bin","sha1":"b6d71e1c96211af46d83278eccf7d6827a57d004","size":83710032},{"file":"Lunar - Silver Star Story Complete (Japan) (Track 2).bin","sha1":"c2406d2953cec7f9fe5826aa71c7359c2baeb3a7","size":491934912},{"file":"Lunar - Silver Star Story Complete (Japan) (Track 3).bin","sha1":"71c3bcfc22038d0f25126b34f703aa464259cfb5","size":25867296},{"file":"Lunar - Silver Star Story Complete (Japan) (Track 4).bin","sha1":"b9c73d3890cd699cb1718131c2f7f04897a14d65","size":1872192}]},"env":{"FAKETIME":"2000-01-01 00:00:00","FAKETIME_DONT_FAKE_MONOTONIC":"1","LD_PRELOAD":"libfaketime.so.1 sha256:7ac8a1cdff1b627548de11513fddedd75014f5b041e9ae9054a634bb04aa7982"},"frame_hash_every":300,"frames":4800,"joypad_ports":2,"observables":["frame_sha1","video_sha1","audio_sha1","state_sha1","state:MPEG.AuthState","state:MPEG.Present","frame_hashes"],"options":{"beetle_saturn_mpeg_card":"enabled"},"presses":[{"button":3,"frame":2400,"length":15},{"button":3,"frame":3000,"length":15},{"button":3,"frame":3600,"length":15},{"button":3,"frame":4200,"length":15}],"repetitions":2,"warmup":0},"kind":"check","migrated_from_version":1,"record_version":2,"recorded_at":"2026-10-27T05:31:33+00:00","subject":"mpeg-card-lunar-cr2-build","supersedes":null,"variants":[{"files_opened":[{"path":"sega_101.bin","sha1_after":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720","sha1_staged":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"},{"path":"mpeg_card.bin","sha1_after":"2f5ebd434f6d5b82e93d03e5626007bd5380f652","sha1_staged":"2f5ebd434f6d5b82e93d03e5626007bd5380f652"}],"files_probed_absent":[],"flags":[],"label":"mpeg_card.bin","loaded":true,"observables":{"audio_sha1":"2ff762359d3d3dd85529ad57c1703be34669a37a","frame_sha1":"4c96f9da044f34b3000d6392f31189293ec78ee9","state:MPEG.AuthState":"02","state:MPEG.Present":"01","state_sha1":"5ef765188b1bd5ce24159ab0c5e49d1fc4ba6040","video_sha1":"f7db10347ce2081c188f83fbe5640131b4f2ceb5"},"repetitions":2,"system_files":{"mpeg_card.bin":"2f5ebd434f6d5b82e93d03e5626007bd5380f652","sega_101.bin":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"},"tracing":"strace"},{"files_opened":[{"path":"sega_101.bin","sha1_after":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720","sha1_staged":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"},{"path":"mpeg_card.bin","sha1_after":"eaac576b8b5e79e7df5eLine truncated
{"check":{"claim":"The mpeg_card.bin image Beetle Saturn loads changes what the Saturn BIOS shows and does for a Video CD.","method":"Saturn BIOS v1.01 (Japan), region forced to Japan, Video CD Card option enabled, automatic RTC left on and the host clock frozen at 2000-01-01 00:00:00 by libfaketime 0.9.10 so runs are deterministic, the authored Video CD inserted. 3000 frames; Saturn A (libretro B) held 15 frames from frame 1300, which is Play on the BIOS CD player, and Start 15 frames from frame 1900. Repeated with each mpeg_card.bin image, with no file, with 512 KiB of zeros and with a 1-byte file; every other input is identical. Each variant runs twice in fresh processes.","observable":"video_sha1","result":{"groups":[{"value":"8fe992a692603f9e0e4cefacc1a8348d17409463","variants":["mpeg_card.bin","mpeg_card.bin.3fee63d4","mpeg_card.bin.80179c49","mpeg_card.bin.821af2bd","mpeg_card.bin.b60e8866","mpeg_card.bin.b7450812","mpeg_card.bin.ee821d3f","mpeg_card.bin.ef9de845","mpeg_card.bin-absent","zeros_512k.bin","one_byte.bin"]}],"statement":"video_sha1 is identical across all 11 variants","status":"same"},"variable":"mpeg_card.bin"},"core":{"build_command":"make -j8","commit":"2ce77e20a956dc9a67b8d3d527f477b984310feb","compiler":["GCC: (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0"],"file":"mednafen_saturn_libretro.so","library_name":"Beetle Saturn","library_version":"v1.32.1","repo":"https://github.com/libretro/beetle-saturn-libretro","sha256":"6e9a44698a2eee492e8d7eff67f2bd5634f52de8cc1af7885f334c3f29fcf2bd"},"emulator":"beetle_saturn","flags":["load average reached 7.02 (threshold 6.0)"],"frontend":{"commit":"8b12685c1652732828c146336a14a5fc265fffd3","script":"scripts/core_probe.py","script_modified":false,"script_sha256":"a95ab54a8d8f9ecc5dc2a55fd0ab93ffc66fe6466e2d89eea56a1a95c17d858b"},"host":{"cpu_count":12,"cpu_model":"13th Gen Intel(R) Core(TM) i9-13900K","kernel":"6.18.54.1-microsoft-standard-WSL2","load_after":[7.02,7.37,5.72],"load_before":[3.87,7.46,5.31],"machine":"x86_64","max_load":6.0,"os":"Linux-6.18.54.1-microsoft-standard-WSL2-x86_64-with-glibc2.39","python":"3.12.3"},"id":"2026-10-27-6afcf7b700ba","inputs":{"content":{"label":"authored Video CD: Big Buck Bunny 60 s, NTSC VCD 2.0 (identity.json beside the image)","sha1":"150cfb81d3c844f0721f2864073a33e326492d46","size":172,"tracks":[{"file":"bbb_vcd.bin","sha1":"e7952e60928dba7f3ceb6c3316894f96ad24e175","size":12169248}]},"env":{"FAKETIME":"2000-01-01 00:00:00","FAKETIME_DONT_FAKE_MONOTONIC":"1","LD_PRELOAD":"libfaketime.so.1 sha256:7ac8a1cdff1b627548de11513fddedd75014f5b041e9ae9054a634bb04aa7982"},"frame_hash_every":150,"frames":3000,"joypad_ports":2,"observables":["frame_sha1","video_sha1","audio_sha1","state_sha1","state:MPEG.AuthState","state:MPEG.Present","frame_hashes"],"options":{"beetle_saturn_mpeg_card":"enabled","beetle_saturn_region":"Japan"},"presses":[{"button":0,"frame":1300,"length":15},{"button":3,"frame":1900,"length":15}],"repetitions":2,"warmup":0},"kind":"check","migrated_from_version":1,"record_version":3,"recorded_at":"2026-10-27T04:57:35+00:00","subject":"mpeg-card-vcd-bios-jp","supersedes":null,"variants":[{"flags":[],"label":"mpeg_card.bin","repetitions":[{"env_unanswered":{"11":1,"13":1,"28":1,"35":3,"37":1,"40":2999,"45":4,"46":1,"47":3000,"51":1,"55":2,"57":1,"8":1,"87":1},"files_opened":[{"path":"sega_101.bin","sha1_after":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720","sha1_staged":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"},{"path":"mpeg_card.bin","sha1_after":"2f5ebd434f6d5b82e93d03e5626007bd5380f652","sha1_staged":"2f5ebd434f6d5b82e93d03e5626007bd5380f652"}],"files_probed_absent":[],"fps":330.3950935700897,"fps_nominal":59.82650314089141,"frames":3000,"input_pressed":60,"input_queries":156000,"last_frame_size":[330,224],"library_name":"Beetle Saturn","library_version":"v1.32.1","loaded":true,"logs":[{"frame":0,"level":"info","text":"Loading \"<content>/bbb_vcd.cue\""},{"frame":0,"level":"debug","text":"Disc 1"},{"frame":0,"level":"debug","text":"- Track 1, LBA: 0 DATA"},{"frame":0,"level":"debug","text":"- Track 2, LBA: 450 DATA"},{"frame":0,"level":"debug","text":"Leadout: 5174"},{"frame":0,"level":"info","text":"Calculating game ID (1 discs)"},{"frame":0,"level":"info","text":"Game ID is: "},{"frame":0,"level":"info","text":"CPU Cache Emulation Mode: Data only"},{"frame":0,"level":"info","text":"Region: 0x1."},{"frame":0,"level":"info","text":"Cart: Backup Memory."},{"frame":0,"level":"info","text":"Controller 1: Control Pad"},{"frame":0,"level":"info","text":"Controller 2: Control Pad"}],"ms_per_frame":3.0266793286621883,"observables":{"audio_sha1":"ebe47d681c9a51cfaa5c9228920e0b7b2c494139","frame_hashes":[[0,"4c96f9da044f34b3000d6392f31189293ec78ee9"],[150,"a189f70d4002cace490f589ea46402fa8444ea47"],[300,"5a4435c3aba5196bcb9ce7b3bcaaae4d49e8f97c"],[450,"5edec5180d1c04dc0ead1e5fbbc7d6d53cc23fdb"],[600,"4c96f9da044f34b3000d6392f31189293ec78ee9"],[750,"4c96f9da044f34b3000d6392f31189293ec78ee9"],[900,"4c96f9da044Line truncated
{"check":{"claim":"The mpeg_card.bin image Beetle Saturn loads changes what the Saturn BIOS shows and does for a Video CD.","method":"Saturn BIOS v1.00a (mpr-17933), region forced to North America, Video CD Card option enabled, automatic RTC left on and the host clock frozen at 2000-01-01 00:00:00 by libfaketime 0.9.10 so runs are deterministic, the authored Video CD inserted. 3000 frames; Saturn A (libretro B) held 15 frames from frame 1300, which is Play on the BIOS CD player, and Start 15 frames from frame 1900. Repeated with each mpeg_card.bin image, with no file, with 512 KiB of zeros and with a 1-byte file; every other input is identical. Each variant runs twice in fresh processes.","observable":"video_sha1","result":{"groups":[{"value":"6f95024fbc87ea46a22be37e04dabe558b900601","variants":["mpeg_card.bin","mpeg_card.bin.3fee63d4","mpeg_card.bin.80179c49","mpeg_card.bin.821af2bd","mpeg_card.bin.b60e8866","mpeg_card.bin.b7450812","mpeg_card.bin.ee821d3f","mpeg_card.bin.ef9de845","mpeg_card.bin-absent","zeros_512k.bin","one_byte.bin"]}],"statement":"video_sha1 is identical across all 11 variants","status":"same"},"variable":"mpeg_card.bin"},"core":{"build_command":"make -j8","commit":"2ce77e20a956dc9a67b8d3d527f477b984310feb","compiler":["GCC: (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0"],"file":"mednafen_saturn_libretro.so","library_name":"Beetle Saturn","library_version":"v1.32.1","repo":"https://github.com/libretro/beetle-saturn-libretro","sha256":"6e9a44698a2eee492e8d7eff67f2bd5634f52de8cc1af7885f334c3f29fcf2bd"},"emulator":"beetle_saturn","flags":["load average reached 8.61 (threshold 6.0)"],"frontend":{"commit":"8b12685c1652732828c146336a14a5fc265fffd3","script":"scripts/core_probe.py","script_modified":false,"script_sha256":"a95ab54a8d8f9ecc5dc2a55fd0ab93ffc66fe6466e2d89eea56a1a95c17d858b"},"host":{"cpu_count":12,"cpu_model":"13th Gen Intel(R) Core(TM) i9-13900K","kernel":"6.18.54.1-microsoft-standard-WSL2","load_after":[8.61,7.5,6.41],"load_before":[3.72,6.03,5.7],"machine":"x86_64","max_load":6.0,"os":"Linux-6.18.54.1-microsoft-standard-WSL2-x86_64-with-glibc2.39","python":"3.12.3"},"id":"2026-10-27-ac66124f74ae","inputs":{"content":{"label":"authored Video CD: Big Buck Bunny 60 s, NTSC VCD 2.0 (identity.json beside the image)","sha1":"150cfb81d3c844f0721f2864073a33e326492d46","size":172,"tracks":[{"file":"bbb_vcd.bin","sha1":"e7952e60928dba7f3ceb6c3316894f96ad24e175","size":12169248}]},"env":{"FAKETIME":"2000-01-01 00:00:00","FAKETIME_DONT_FAKE_MONOTONIC":"1","LD_PRELOAD":"libfaketime.so.1 sha256:7ac8a1cdff1b627548de11513fddedd75014f5b041e9ae9054a634bb04aa7982"},"frame_hash_every":150,"frames":3000,"joypad_ports":2,"observables":["frame_sha1","video_sha1","audio_sha1","state_sha1","state:MPEG.AuthState","state:MPEG.Present","frame_hashes"],"options":{"beetle_saturn_mpeg_card":"enabled","beetle_saturn_region":"North America"},"presses":[{"button":0,"frame":1300,"length":15},{"button":3,"frame":1900,"length":15}],"repetitions":2,"warmup":0},"kind":"check","migrated_from_version":1,"record_version":3,"recorded_at":"2026-10-27T05:07:13+00:00","subject":"mpeg-card-vcd-bios-us","supersedes":null,"variants":[{"flags":[],"label":"mpeg_card.bin","repetitions":[{"env_unanswered":{"11":1,"13":1,"28":1,"35":3,"37":1,"40":2999,"45":4,"46":1,"47":3000,"51":1,"55":2,"57":1,"8":1,"87":1},"files_opened":[{"path":"mpr-17933.bin","sha1_after":"faa8ea183a6d7bbe5d4e03bb1332519800d3fbc3","sha1_staged":"faa8ea183a6d7bbe5d4e03bb1332519800d3fbc3"},{"path":"mpeg_card.bin","sha1_after":"2f5ebd434f6d5b82e93d03e5626007bd5380f652","sha1_staged":"2f5ebd434f6d5b82e93d03e5626007bd5380f652"}],"files_probed_absent":[],"fps":244.16338538684496,"fps_nominal":59.82650314089141,"frames":3000,"input_pressed":60,"input_queries":156000,"last_frame_size":[330,224],"library_name":"Beetle Saturn","library_version":"v1.32.1","loaded":true,"logs":[{"frame":0,"level":"info","text":"Loading \"<content>/bbb_vcd.cue\""},{"frame":0,"level":"debug","text":"Disc 1"},{"frame":0,"level":"debug","text":"- Track 1, LBA: 0 DATA"},{"frame":0,"level":"debug","text":"- Track 2, LBA: 450 DATA"},{"frame":0,"level":"debug","text":"Leadout: 5174"},{"frame":0,"level":"info","text":"Calculating game ID (1 discs)"},{"frame":0,"level":"info","text":"Game ID is: "},{"frame":0,"level":"info","text":"CPU Cache Emulation Mode: Data only"},{"frame":0,"level":"info","text":"Region: 0x4."},{"frame":0,"level":"info","text":"Cart: Backup Memory."},{"frame":0,"level":"info","text":"Controller 1: Control Pad"},{"frame":0,"level":"info","text":"Controller 2: Control Pad"}],"ms_per_frame":4.095618179669448,"observables":{"audio_sha1":"9c8887d4ee16309b6da657ede701a90a97f1e61a","frame_hashes":[[0,"4c96f9da044f34b3000d6392f31189293ec78ee9"],[150,"e9725385e2865e6448bbbc709fc72e54f5478569"],[300,"a87b28c1dca58cf41d1c5c892be3a14eb0c2f7ff"],[450,"f6f6ad7714da4b7c32a7736276c8cba7ef9d45d0"],[600,"4c96f9da044f34b3000d6392f31189293ec78ee9"],[750,"4c96f9da044f34b3000d6392f31189293ec78ee9Line truncated
{"check":{"claim":"The mpeg_card.bin image Beetle Saturn loads changes what the Saturn BIOS shows and does for a Video CD.","method":"Experimental build, not what users run: mednafen/ss/cdb.c reads the AUTH_DEVICE and GET_AUTH device type from CR2 (CTR.CD[1]) instead of CR3 (CTR.CD[2]), plus a command trace to a file. Saturn BIOS v1.01 (Japan), region forced to Japan, Video CD Card option enabled, automatic RTC left on and the host clock frozen at 2000-01-01 00:00:00 by libfaketime 0.9.10 so runs are deterministic, the authored Video CD inserted. 3000 frames; Saturn A (libretro B) held 15 frames from frame 1300, which is Play on the BIOS CD player, and Start 15 frames from frame 1900. Repeated with each mpeg_card.bin image, with no file, with 512 KiB of zeros and with a 1-byte file; every other input is identical. Each variant runs twice in fresh processes.","observable":"video_sha1","result":{"groups":[{"value":"88683d404f1266d0177c73f970cbd5996c11b7c1","variants":["mpeg_card.bin","mpeg_card.bin.3fee63d4","mpeg_card.bin.80179c49","mpeg_card.bin.821af2bd","mpeg_card.bin.b60e8866","mpeg_card.bin.b7450812","mpeg_card.bin.ee821d3f","mpeg_card.bin.ef9de845","zeros_512k.bin","one_byte.bin"]},{"value":"4283d78ac91cfef738daf1fe682dff4e531809e4","variants":["mpeg_card.bin-absent"]}],"statement":"video_sha1 takes 2 distinct values across 11 variants","status":"differs"},"variable":"mpeg_card.bin"},"core":{"build_command":"make -j8 after editing mednafen/ss/cdb.c: AUTH_DEVICE and GET_AUTH read CTR.CD[1] instead of CTR.CD[2], plus a command trace","commit":"2ce77e20a956dc9a67b8d3d527f477b984310feb","compiler":["GCC: (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0"],"file":"mednafen_saturn_libretro.so","library_name":"Beetle Saturn","library_version":"v1.32.1","repo":"https://github.com/libretro/beetle-saturn-libretro","sha256":"fc9c33c697ddcde7547f2315f6ff91231f2ecfa24280c884a0185b880ffc2dfb"},"emulator":"beetle_saturn","flags":[],"frontend":{"commit":"8b12685c1652732828c146336a14a5fc265fffd3","script":"scripts/core_probe.py","script_modified":false,"script_sha256":"a95ab54a8d8f9ecc5dc2a55fd0ab93ffc66fe6466e2d89eea56a1a95c17d858b"},"host":{"cpu_count":12,"cpu_model":"13th Gen Intel(R) Core(TM) i9-13900K","kernel":"6.18.54.1-microsoft-standard-WSL2","load_after":[2.41,4.58,5.69],"load_before":[3.24,7.0,6.58],"machine":"x86_64","max_load":6.0,"os":"Linux-6.18.54.1-microsoft-standard-WSL2-x86_64-with-glibc2.39","python":"3.12.3"},"id":"2026-10-27-43527b763ace","inputs":{"content":{"label":"authored Video CD: Big Buck Bunny 60 s, NTSC VCD 2.0 (identity.json beside the image)","sha1":"150cfb81d3c844f0721f2864073a33e326492d46","size":172,"tracks":[{"file":"bbb_vcd.bin","sha1":"e7952e60928dba7f3ceb6c3316894f96ad24e175","size":12169248}]},"env":{"FAKETIME":"2000-01-01 00:00:00","FAKETIME_DONT_FAKE_MONOTONIC":"1","LD_PRELOAD":"libfaketime.so.1 sha256:7ac8a1cdff1b627548de11513fddedd75014f5b041e9ae9054a634bb04aa7982"},"frame_hash_every":150,"frames":3000,"joypad_ports":2,"observables":["frame_sha1","video_sha1","audio_sha1","state_sha1","state:MPEG.AuthState","state:MPEG.Present","frame_hashes"],"options":{"beetle_saturn_mpeg_card":"enabled","beetle_saturn_region":"Japan"},"presses":[{"button":0,"frame":1300,"length":15},{"button":3,"frame":1900,"length":15}],"repetitions":2,"warmup":0},"kind":"check","migrated_from_version":1,"record_version":3,"recorded_at":"2026-10-27T05:14:32+00:00","subject":"mpeg-card-vcd-bios-jp-cr2-build","supersedes":null,"variants":[{"flags":[],"label":"mpeg_card.bin","repetitions":[{"env_unanswered":{"11":1,"13":1,"28":1,"35":3,"37":1,"40":2999,"45":4,"46":1,"47":3000,"51":1,"55":2,"57":1,"8":1,"87":1},"files_opened":[{"path":"sega_101.bin","sha1_after":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720","sha1_staged":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"},{"path":"mpeg_card.bin","sha1_after":"2f5ebd434f6d5b82e93d03e5626007bd5380f652","sha1_staged":"2f5ebd434f6d5b82e93d03e5626007bd5380f652"}],"files_probed_absent":[],"fps":337.8442323774398,"fps_nominal":59.82650314089141,"frames":3000,"input_pressed":60,"input_queries":156000,"last_frame_size":[330,224],"library_name":"Beetle Saturn","library_version":"v1.32.1","loaded":true,"logs":[{"frame":0,"level":"info","text":"Loading \"<content>/bbb_vcd.cue\""},{"frame":0,"level":"debug","text":"Disc 1"},{"frame":0,"level":"debug","text":"- Track 1, LBA: 0 DATA"},{"frame":0,"level":"debug","text":"- Track 2, LBA: 450 DATA"},{"frame":0,"level":"debug","text":"Leadout: 5174"},{"frame":0,"level":"info","text":"Calculating game ID (1 discs)"},{"frame":0,"level":"info","text":"Game ID is: "},{"frame":0,"level":"info","text":"CPU Cache Emulation Mode: Data only"},{"frame":0,"level":"info","text":"Region: 0x1."},{"frame":0,"level":"info","text":"Cart: Backup Memory."},{"frame":0,"level":"info","text":"Controller 1: Control Pad"},{"frame":0,"level":"info","text":"Controller 2: Control Pad"}],"ms_per_frame":2.9599439746622616,"observables":{"audio_sha1":"85f24b268528b103056d511Line truncated
{"check":{"claim":"The mpeg_card.bin image Beetle Saturn loads changes what the Saturn BIOS shows and does for a Video CD.","method":"Saturn BIOS v1.01 (Japan), region forced to Japan, Video CD Card option enabled, automatic RTC left on and the host clock running (no libfaketime), to test whether runs repeat, the authored Video CD inserted. 3000 frames; Saturn A (libretro B) held 15 frames from frame 1300, which is Play on the BIOS CD player, and Start 15 frames from frame 1900. Repeated with each mpeg_card.bin image, with no file, with 512 KiB of zeros and with a 1-byte file; every other input is identical. Each variant runs twice in fresh processes.","observable":"video_sha1","result":{"groups":[{"value":"bde95c486614dc797ba5a939fb738d947b479fa8","variants":["mpeg_card.bin"]},{"value":"5a14993408a486a4c55341ca7284f89e148dfc18","variants":["mpeg_card.bin.3fee63d4"]},{"value":"7e3d403552c31c494beb9847262bf86cf266c3fa","variants":["mpeg_card.bin.80179c49"]},{"value":"b639d8c3130638876b398c66f463b98f535a2497","variants":["mpeg_card.bin.821af2bd"]},{"value":"5dd8db90027d8ef7670c0805751b8973920195a9","variants":["mpeg_card.bin.b60e8866"]},{"value":"fabe749bf34c09de4c26d4328e2744876488a18e","variants":["mpeg_card.bin.b7450812"]},{"value":"ef9d0fab7815222344d7d669bd0dbc6e0d811174","variants":["mpeg_card.bin.ee821d3f"]},{"value":"164f2e824cc15672226bebc07998f40d5e3422ce","variants":["mpeg_card.bin.ef9de845"]},{"value":"1c920f21c6fc3d402fa45a7a5fb4d302a82d4898","variants":["mpeg_card.bin-absent"]},{"value":"2a6d6e171b0bafe5b60ec41a37db4322ff2b12e8","variants":["zeros_512k.bin"]},{"value":"2d5be7af25846153e20081a8b12b614ff348c4b9","variants":["one_byte.bin"]}],"statement":"video_sha1 is not settled in 11 of 11 variants (mpeg_card.bin, mpeg_card.bin.3fee63d4, mpeg_card.bin.80179c49, mpeg_card.bin.821af2bd, mpeg_card.bin.b60e8866, mpeg_card.bin.b7450812, mpeg_card.bin.ee821d3f, mpeg_card.bin.ef9de845, mpeg_card.bin-absent, zeros_512k.bin, one_byte.bin): no comparison is drawn","status":"inconclusive"},"variable":"mpeg_card.bin"},"core":{"build_command":"make -j8","commit":"2ce77e20a956dc9a67b8d3d527f477b984310feb","compiler":["GCC: (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0"],"file":"mednafen_saturn_libretro.so","library_name":"Beetle Saturn","library_version":"v1.32.1","repo":"https://github.com/libretro/beetle-saturn-libretro","sha256":"6e9a44698a2eee492e8d7eff67f2bd5634f52de8cc1af7885f334c3f29fcf2bd"},"emulator":"beetle_saturn","flags":["mpeg_card.bin: repetition 1 disagrees with repetition 0 on frame_hashes, frame_sha1, state_sha1, video_sha1","mpeg_card.bin.3fee63d4: repetition 1 disagrees with repetition 0 on frame_hashes, frame_sha1, state_sha1, video_sha1","mpeg_card.bin.80179c49: repetition 1 disagrees with repetition 0 on frame_hashes, frame_sha1, state_sha1, video_sha1","mpeg_card.bin.821af2bd: repetition 1 disagrees with repetition 0 on frame_hashes, frame_sha1, state_sha1, video_sha1","mpeg_card.bin.b60e8866: repetition 1 disagrees with repetition 0 on frame_hashes, frame_sha1, state_sha1, video_sha1","mpeg_card.bin.b7450812: repetition 1 disagrees with repetition 0 on frame_hashes, frame_sha1, state_sha1, video_sha1","mpeg_card.bin.ee821d3f: repetition 1 disagrees with repetition 0 on frame_hashes, frame_sha1, state_sha1, video_sha1","mpeg_card.bin.ef9de845: repetition 1 disagrees with repetition 0 on frame_hashes, frame_sha1, state_sha1, video_sha1","mpeg_card.bin-absent: repetition 1 disagrees with repetition 0 on frame_hashes, frame_sha1, state_sha1, video_sha1","zeros_512k.bin: repetition 1 disagrees with repetition 0 on frame_hashes, frame_sha1, state_sha1, video_sha1","one_byte.bin: repetition 1 disagrees with repetition 0 on frame_hashes, frame_sha1, state_sha1, video_sha1"],"frontend":{"commit":"8b12685c1652732828c146336a14a5fc265fffd3","script":"scripts/core_probe.py","script_modified":false,"script_sha256":"a95ab54a8d8f9ecc5dc2a55fd0ab93ffc66fe6466e2d89eea56a1a95c17d858b"},"host":{"cpu_count":12,"cpu_model":"13th Gen Intel(R) Core(TM) i9-13900K","kernel":"6.18.54.1-microsoft-standard-WSL2","load_after":[1.77,2.58,4.32],"load_before":[1.81,3.2,4.89],"machine":"x86_64","max_load":6.0,"os":"Linux-6.18.54.1-microsoft-standard-WSL2-x86_64-with-glibc2.39","python":"3.12.3"},"id":"2026-10-27-bf3d8283b265","inputs":{"content":{"label":"authored Video CD: Big Buck Bunny 60 s, NTSC VCD 2.0 (identity.json beside the image)","sha1":"150cfb81d3c844f0721f2864073a33e326492d46","size":172,"tracks":[{"file":"bbb_vcd.bin","sha1":"e7952e60928dba7f3ceb6c3316894f96ad24e175","size":12169248}]},"env":{},"frame_hash_every":150,"frames":3000,"joypad_ports":2,"observables":["frame_sha1","video_sha1","audio_sha1","state_sha1","state:MPEG.AuthState","state:MPEG.Present","frame_hashes"],"options":{"beetle_saturn_mpeg_card":"enabled","beetle_saturn_region":"Japan"},"presses":[{"button":0,"frame":1300,"length":15},{"button":3,"frame":1900,"length":15}],"repetitions":2,"warmup":0},"kind":"check","migrated_from_version":1,"recorLine truncated
{"check":{"claim":"The mpeg_card.bin image Beetle Saturn loads decides whether Lunar - Silver Star Story MPEG (T-27904G) detects the Video CD Card and plays its movies.","method":"Saturn BIOS v1.01 (Japan), Video CD Card option enabled, region auto-detected from the disc (Japan), host clock frozen at 2000-01-01 00:00:00 by libfaketime 0.9.10 so runs are deterministic. 4800 frames; Start held 15 frames at frames 2400, 3000, 3600 and 4200. Repeated with each mpeg_card.bin image, with no file, with 512 KiB of zeros and with a 1-byte file; every other input is identical. Each variant runs twice in fresh processes.","observable":"video_sha1","result":{"groups":[{"value":"e248465e55f8edbdfb16a8ca6588817ffa9d8064","variants":["mpeg_card.bin","mpeg_card.bin.3fee63d4","mpeg_card.bin.80179c49","mpeg_card.bin.821af2bd","mpeg_card.bin.b60e8866","mpeg_card.bin.b7450812","mpeg_card.bin.ee821d3f","mpeg_card.bin.ef9de845","mpeg_card.bin-absent","zeros_512k.bin","one_byte.bin"]}],"statement":"video_sha1 is identical across all 11 variants","status":"same"},"variable":"mpeg_card.bin"},"core":{"build_command":"make -j8","commit":"2ce77e20a956dc9a67b8d3d527f477b984310feb","compiler":["GCC: (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0"],"file":"mednafen_saturn_libretro.so","library_name":"Beetle Saturn","library_version":"v1.32.1","repo":"https://github.com/libretro/beetle-saturn-libretro","sha256":"6e9a44698a2eee492e8d7eff67f2bd5634f52de8cc1af7885f334c3f29fcf2bd"},"emulator":"beetle_saturn","flags":[],"frontend":{"commit":"8b12685c1652732828c146336a14a5fc265fffd3","script":"scripts/core_probe.py","script_modified":false,"script_sha256":"a95ab54a8d8f9ecc5dc2a55fd0ab93ffc66fe6466e2d89eea56a1a95c17d858b"},"host":{"cpu_count":12,"cpu_model":"13th Gen Intel(R) Core(TM) i9-13900K","kernel":"6.18.54.1-microsoft-standard-WSL2","load_after":[3.16,2.62,3.83],"load_before":[1.77,2.58,4.32],"machine":"x86_64","max_load":6.0,"os":"Linux-6.18.54.1-microsoft-standard-WSL2-x86_64-with-glibc2.39","python":"3.12.3"},"id":"2026-10-27-5d8f0b375830","inputs":{"content":{"label":"Redump: Lunar - Silver Star Story Complete (Japan), T-27904G V1.00 (cue sha1 7a609a9b579de648197ccfb5ab7bf4b3beb07d95)","sha1":"7a609a9b579de648197ccfb5ab7bf4b3beb07d95","size":554,"tracks":[{"file":"Lunar - Silver Star Story Complete (Japan) (Track 1).bin","sha1":"b6d71e1c96211af46d83278eccf7d6827a57d004","size":83710032},{"file":"Lunar - Silver Star Story Complete (Japan) (Track 2).bin","sha1":"c2406d2953cec7f9fe5826aa71c7359c2baeb3a7","size":491934912},{"file":"Lunar - Silver Star Story Complete (Japan) (Track 3).bin","sha1":"71c3bcfc22038d0f25126b34f703aa464259cfb5","size":25867296},{"file":"Lunar - Silver Star Story Complete (Japan) (Track 4).bin","sha1":"b9c73d3890cd699cb1718131c2f7f04897a14d65","size":1872192}]},"env":{"FAKETIME":"2000-01-01 00:00:00","FAKETIME_DONT_FAKE_MONOTONIC":"1","LD_PRELOAD":"libfaketime.so.1 sha256:7ac8a1cdff1b627548de11513fddedd75014f5b041e9ae9054a634bb04aa7982"},"frame_hash_every":300,"frames":4800,"joypad_ports":2,"observables":["frame_sha1","video_sha1","audio_sha1","state_sha1","state:MPEG.AuthState","state:MPEG.Present","frame_hashes"],"options":{"beetle_saturn_mpeg_card":"enabled"},"presses":[{"button":3,"frame":2400,"length":15},{"button":3,"frame":3000,"length":15},{"button":3,"frame":3600,"length":15},{"button":3,"frame":4200,"length":15}],"repetitions":2,"warmup":0},"kind":"check","migrated_from_version":1,"record_version":3,"recorded_at":"2026-10-27T05:26:27+00:00","subject":"mpeg-card-lunar","supersedes":null,"variants":[{"flags":[],"label":"mpeg_card.bin","repetitions":[{"env_unanswered":{"11":1,"13":1,"28":1,"35":3,"37":1,"40":4799,"45":4,"46":1,"47":4800,"51":1,"55":2,"57":1,"8":1,"87":1},"files_opened":[{"path":"sega_101.bin","sha1_after":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720","sha1_staged":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"},{"path":"mpeg_card.bin","sha1_after":"2f5ebd434f6d5b82e93d03e5626007bd5380f652","sha1_staged":"2f5ebd434f6d5b82e93d03e5626007bd5380f652"}],"files_probed_absent":[],"fps":366.16169342725783,"fps_nominal":59.82650314089141,"frames":4800,"input_pressed":120,"input_queries":249600,"last_frame_size":[330,224],"library_name":"Beetle Saturn","library_version":"v1.32.1","loaded":true,"logs":[{"frame":0,"level":"info","text":"Loading \"<content>/Lunar - Silver Star Story Complete (Japan).cue\""},{"frame":0,"level":"debug","text":"Disc 1"},{"frame":0,"level":"debug","text":"- Track 1, LBA: 0 DATA"},{"frame":0,"level":"debug","text":"- Track 2, LBA: 35891 DATA"},{"frame":0,"level":"debug","text":"- Track 3, LBA: 244897 AUDIO"},{"frame":0,"level":"debug","text":"- Track 4, LBA: 255894 AUDIO"},{"frame":0,"level":"debug","text":"Leadout: 256541"},{"frame":0,"level":"info","text":"Calculating game ID (1 discs)"},{"frame":0,"level":"info","text":"Game ID is: T-27904G"},{"frame":0,"level":"info","text":"This is a Saturn disc."},{"frame":0,"level":"info","text":"Disc Region: \"Japan\""},{"frame":0,"level":"inLine truncated
{"check":{"claim":"The mpeg_card.bin image Beetle Saturn loads decides whether Lunar - Silver Star Story MPEG (T-27904G) detects the Video CD Card and plays its movies.","method":"Experimental build, not what users run: AUTH_DEVICE and GET_AUTH read the device type from CR2. Saturn BIOS v1.01 (Japan), Video CD Card option enabled, region auto-detected from the disc (Japan), host clock frozen at 2000-01-01 00:00:00 by libfaketime 0.9.10 so runs are deterministic. 4800 frames; Start held 15 frames at frames 2400, 3000, 3600 and 4200. Repeated with each mpeg_card.bin image, with no file, with 512 KiB of zeros and with a 1-byte file; every other input is identical. Each variant runs twice in fresh processes.","observable":"video_sha1","result":{"groups":[{"value":"f7db10347ce2081c188f83fbe5640131b4f2ceb5","variants":["mpeg_card.bin","mpeg_card.bin.3fee63d4","mpeg_card.bin.80179c49","mpeg_card.bin.821af2bd","mpeg_card.bin.b60e8866","mpeg_card.bin.b7450812","mpeg_card.bin.ee821d3f","mpeg_card.bin.ef9de845","zeros_512k.bin","one_byte.bin"]},{"value":"d60fcae489df0c317cca4e95f6f57c323be20ee4","variants":["mpeg_card.bin-absent"]}],"statement":"video_sha1 takes 2 distinct values across 11 variants","status":"differs"},"variable":"mpeg_card.bin"},"core":{"build_command":"make -j8 after editing mednafen/ss/cdb.c: AUTH_DEVICE and GET_AUTH read CTR.CD[1] instead of CTR.CD[2], plus a command trace","commit":"2ce77e20a956dc9a67b8d3d527f477b984310feb","compiler":["GCC: (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0"],"file":"mednafen_saturn_libretro.so","library_name":"Beetle Saturn","library_version":"v1.32.1","repo":"https://github.com/libretro/beetle-saturn-libretro","sha256":"fc9c33c697ddcde7547f2315f6ff91231f2ecfa24280c884a0185b880ffc2dfb"},"emulator":"beetle_saturn","flags":[],"frontend":{"commit":"8b12685c1652732828c146336a14a5fc265fffd3","script":"scripts/core_probe.py","script_modified":false,"script_sha256":"a95ab54a8d8f9ecc5dc2a55fd0ab93ffc66fe6466e2d89eea56a1a95c17d858b"},"host":{"cpu_count":12,"cpu_model":"13th Gen Intel(R) Core(TM) i9-13900K","kernel":"6.18.54.1-microsoft-standard-WSL2","load_after":[3.24,2.85,3.56],"load_before":[3.16,2.62,3.83],"machine":"x86_64","max_load":6.0,"os":"Linux-6.18.54.1-microsoft-standard-WSL2-x86_64-with-glibc2.39","python":"3.12.3"},"id":"2026-10-27-f532e5006714","inputs":{"content":{"label":"Redump: Lunar - Silver Star Story Complete (Japan), T-27904G V1.00 (cue sha1 7a609a9b579de648197ccfb5ab7bf4b3beb07d95)","sha1":"7a609a9b579de648197ccfb5ab7bf4b3beb07d95","size":554,"tracks":[{"file":"Lunar - Silver Star Story Complete (Japan) (Track 1).bin","sha1":"b6d71e1c96211af46d83278eccf7d6827a57d004","size":83710032},{"file":"Lunar - Silver Star Story Complete (Japan) (Track 2).bin","sha1":"c2406d2953cec7f9fe5826aa71c7359c2baeb3a7","size":491934912},{"file":"Lunar - Silver Star Story Complete (Japan) (Track 3).bin","sha1":"71c3bcfc22038d0f25126b34f703aa464259cfb5","size":25867296},{"file":"Lunar - Silver Star Story Complete (Japan) (Track 4).bin","sha1":"b9c73d3890cd699cb1718131c2f7f04897a14d65","size":1872192}]},"env":{"FAKETIME":"2000-01-01 00:00:00","FAKETIME_DONT_FAKE_MONOTONIC":"1","LD_PRELOAD":"libfaketime.so.1 sha256:7ac8a1cdff1b627548de11513fddedd75014f5b041e9ae9054a634bb04aa7982"},"frame_hash_every":300,"frames":4800,"joypad_ports":2,"observables":["frame_sha1","video_sha1","audio_sha1","state_sha1","state:MPEG.AuthState","state:MPEG.Present","frame_hashes"],"options":{"beetle_saturn_mpeg_card":"enabled"},"presses":[{"button":3,"frame":2400,"length":15},{"button":3,"frame":3000,"length":15},{"button":3,"frame":3600,"length":15},{"button":3,"frame":4200,"length":15}],"repetitions":2,"warmup":0},"kind":"check","migrated_from_version":1,"record_version":3,"recorded_at":"2026-10-27T05:31:33+00:00","subject":"mpeg-card-lunar-cr2-build","supersedes":null,"variants":[{"flags":[],"label":"mpeg_card.bin","repetitions":[{"env_unanswered":{"11":1,"13":1,"28":1,"35":3,"37":1,"40":4799,"45":4,"46":1,"47":4800,"51":1,"55":2,"57":1,"8":1,"87":1},"files_opened":[{"path":"sega_101.bin","sha1_after":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720","sha1_staged":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"},{"path":"mpeg_card.bin","sha1_after":"2f5ebd434f6d5b82e93d03e5626007bd5380f652","sha1_staged":"2f5ebd434f6d5b82e93d03e5626007bd5380f652"}],"files_probed_absent":[],"fps":363.57766936404164,"fps_nominal":59.82650314089141,"frames":4800,"input_pressed":120,"input_queries":249600,"last_frame_size":[330,224],"library_name":"Beetle Saturn","library_version":"v1.32.1","loaded":true,"logs":[{"frame":0,"level":"info","text":"Loading \"<content>/Lunar - Silver Star Story Complete (Japan).cue\""},{"frame":0,"level":"debug","text":"Disc 1"},{"frame":0,"level":"debug","text":"- Track 1, LBA: 0 DATA"},{"frame":0,"level":"debug","text":"- Track 2, LBA: 35891 DATA"},{"frame":0,"level":"debug","text":"- Track 3, LBA: 244897 AUDIO"},{"frame":0,"level":"debug","text":"- Track 4, LBA: 255894 AUDIO"},{"frame":0,"level"Line truncated
+1 -1
View File
@@ -1 +1 @@
{"check":{"claim":"The mpeg_card.bin image Yabause hands to the Saturn BIOS through the Get MPEG ROM command (0xE2) changes what the BIOS shows and does for a Video CD.","method":"Experimental build, not what RetroArch users run: libretro Yabause with yinit.mpegpath pointed at the system directory, which reaches the same cs2.c Cs2GetMPEGRom path that standalone Yabause uses when its MpegROM/Path setting is filled. Saturn BIOS v1.01 (Japan), one emulation thread, host clock frozen at 2000-01-01 00:00:00 by libfaketime 0.9.10 so runs are deterministic, the authored Video CD inserted. 3000 frames; the button the core maps to Saturn A held 15 frames from frame 1300 (Play on the BIOS CD player) and Start 15 frames from frame 1900. Repeated with each mpeg_card.bin image, with no file, with 512 KiB of zeros and with a 1-byte file; every other input is identical. Each variant runs twice in fresh processes.","observable":"video_sha1","result":{"groups":[{"value":"c89177305da310bd329a17d0f4a5e60ffe5cdece","variants":["mpeg_card.bin","mpeg_card.bin.3fee63d4","mpeg_card.bin.80179c49","mpeg_card.bin.821af2bd","mpeg_card.bin.b60e8866","mpeg_card.bin.b7450812","mpeg_card.bin.ee821d3f","mpeg_card.bin.ef9de845","mpeg_card.bin-absent","zeros_512k.bin","one_byte.bin"]}],"statement":"video_sha1 is identical across all 11 variants","status":"same"},"variable":"mpeg_card.bin"},"core":{"build_command":"make -j8 in yabause/src/libretro, after setting yinit.mpegpath to <system>/mpeg_card.bin in libretro.c (the port sets it to NULL)","commit":"8926b0c6c347f8c5c755911ddb0ac695420ffbf8","compiler":["GCC: (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0"],"file":"yabause_libretro.so","library_name":"Yabause","library_version":"v0.9.15 625bee35c","repo":"https://github.com/libretro/yabause","sha256":"1530b0a3c7f2ff762e4b02e22f016be6753a7f3864306cf17ca810bccee0e5c4"},"data":{"bytes":2962,"sha256":"e746187f0118ee5f4ed90458021bb36c215b723f7ef36198fb25d1e40e008fe5"},"emulator":"yabause","flags":[],"frontend":{"commit":"8b12685c1652732828c146336a14a5fc265fffd3","script":"scripts/core_probe.py","script_modified":false,"script_sha256":"a95ab54a8d8f9ecc5dc2a55fd0ab93ffc66fe6466e2d89eea56a1a95c17d858b"},"host":{"cpu_count":12,"cpu_model":"13th Gen Intel(R) Core(TM) i9-13900K","kernel":"6.18.54.1-microsoft-standard-WSL2","load_after":[1.81,3.2,4.89],"load_before":[2.41,4.58,5.69],"machine":"x86_64","max_load":6.0,"os":"Linux-6.18.54.1-microsoft-standard-WSL2-x86_64-with-glibc2.39","python":"3.12.3"},"id":"2026-10-27-92c9da074bcb","inputs":{"content":{"label":"authored Video CD: Big Buck Bunny 60 s, NTSC VCD 2.0 (identity.json beside the image)","sha1":"150cfb81d3c844f0721f2864073a33e326492d46","size":172,"tracks":[{"file":"bbb_vcd.bin","sha1":"e7952e60928dba7f3ceb6c3316894f96ad24e175","size":12169248}]},"env":{"FAKETIME":"2000-01-01 00:00:00","FAKETIME_DONT_FAKE_MONOTONIC":"1","LD_PRELOAD":"libfaketime.so.1 sha256:7ac8a1cdff1b627548de11513fddedd75014f5b041e9ae9054a634bb04aa7982"},"frame_hash_every":150,"frames":3000,"joypad_ports":2,"observables":["frame_sha1","video_sha1","audio_sha1","state_sha1","frame_hashes"],"options":{"yabause_numthreads":"1"},"presses":[{"button":0,"frame":1300,"length":15},{"button":3,"frame":1900,"length":15}],"repetitions":2,"warmup":0},"kind":"check","migrated_from_version":1,"record_version":2,"recorded_at":"2026-10-27T05:18:19+00:00","subject":"mpeg-card-vcd-bios-jp-mpegpath-build","supersedes":null,"variants":[{"files_opened":[{"path":"sega_101.bin","sha1_after":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720","sha1_staged":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"},{"path":"mpeg_card.bin","sha1_after":"2f5ebd434f6d5b82e93d03e5626007bd5380f652","sha1_staged":"2f5ebd434f6d5b82e93d03e5626007bd5380f652"}],"files_probed_absent":[],"flags":[],"label":"mpeg_card.bin","loaded":true,"observables":{"audio_sha1":"8eefdabcbbb9f662de4b0d04087df5bd18ba6e45","frame_sha1":"1b5cd15c0e874315a9df0cd89c790dac0d4ca672","state_sha1":"1a9a2509e0e0574373a97c243c6bebededd63cd8","video_sha1":"c89177305da310bd329a17d0f4a5e60ffe5cdece"},"repetitions":2,"system_files":{"mpeg_card.bin":"2f5ebd434f6d5b82e93d03e5626007bd5380f652","sega_101.bin":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"},"tracing":"strace"},{"files_opened":[{"path":"sega_101.bin","sha1_after":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720","sha1_staged":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"},{"path":"mpeg_card.bin","sha1_after":"eaac576b8b5e79e7df5e3efd70275b220ff70d78","sha1_staged":"eaac576b8b5e79e7df5e3efd70275b220ff70d78"}],"files_probed_absent":[],"flags":[],"label":"mpeg_card.bin.3fee63d4","loaded":true,"observables":{"audio_sha1":"8eefdabcbbb9f662de4b0d04087df5bd18ba6e45","frame_sha1":"1b5cd15c0e874315a9df0cd89c790dac0d4ca672","state_sha1":"1a9a2509e0e0574373a97c243c6bebededd63cd8","video_sha1":"c89177305da310bd329a17d0f4a5e60ffe5cdece"},"repetitions":2,"system_files":{"mpeg_card.bin":"eaac576b8b5e79e7df5e3efd70275b220ff70d78","sega_101.bin":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"}Line truncated
{"check":{"claim":"The mpeg_card.bin image Yabause hands to the Saturn BIOS through the Get MPEG ROM command (0xE2) changes what the BIOS shows and does for a Video CD.","method":"Experimental build, not what RetroArch users run: libretro Yabause with yinit.mpegpath pointed at the system directory, which reaches the same cs2.c Cs2GetMPEGRom path that standalone Yabause uses when its MpegROM/Path setting is filled. Saturn BIOS v1.01 (Japan), one emulation thread, host clock frozen at 2000-01-01 00:00:00 by libfaketime 0.9.10 so runs are deterministic, the authored Video CD inserted. 3000 frames; the button the core maps to Saturn A held 15 frames from frame 1300 (Play on the BIOS CD player) and Start 15 frames from frame 1900. Repeated with each mpeg_card.bin image, with no file, with 512 KiB of zeros and with a 1-byte file; every other input is identical. Each variant runs twice in fresh processes.","observable":"video_sha1","result":{"groups":[{"value":"c89177305da310bd329a17d0f4a5e60ffe5cdece","variants":["mpeg_card.bin","mpeg_card.bin.3fee63d4","mpeg_card.bin.80179c49","mpeg_card.bin.821af2bd","mpeg_card.bin.b60e8866","mpeg_card.bin.b7450812","mpeg_card.bin.ee821d3f","mpeg_card.bin.ef9de845","mpeg_card.bin-absent","zeros_512k.bin","one_byte.bin"]}],"statement":"video_sha1 is identical across all 11 variants","status":"same"},"variable":"mpeg_card.bin"},"core":{"build_command":"make -j8 in yabause/src/libretro, after setting yinit.mpegpath to <system>/mpeg_card.bin in libretro.c (the port sets it to NULL)","commit":"8926b0c6c347f8c5c755911ddb0ac695420ffbf8","compiler":["GCC: (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0"],"file":"yabause_libretro.so","library_name":"Yabause","library_version":"v0.9.15 625bee35c","repo":"https://github.com/libretro/yabause","sha256":"1530b0a3c7f2ff762e4b02e22f016be6753a7f3864306cf17ca810bccee0e5c4"},"emulator":"yabause","flags":[],"frontend":{"commit":"8b12685c1652732828c146336a14a5fc265fffd3","script":"scripts/core_probe.py","script_modified":false,"script_sha256":"a95ab54a8d8f9ecc5dc2a55fd0ab93ffc66fe6466e2d89eea56a1a95c17d858b"},"host":{"cpu_count":12,"cpu_model":"13th Gen Intel(R) Core(TM) i9-13900K","kernel":"6.18.54.1-microsoft-standard-WSL2","load_after":[1.81,3.2,4.89],"load_before":[2.41,4.58,5.69],"machine":"x86_64","max_load":6.0,"os":"Linux-6.18.54.1-microsoft-standard-WSL2-x86_64-with-glibc2.39","python":"3.12.3"},"id":"2026-10-27-92c9da074bcb","inputs":{"content":{"label":"authored Video CD: Big Buck Bunny 60 s, NTSC VCD 2.0 (identity.json beside the image)","sha1":"150cfb81d3c844f0721f2864073a33e326492d46","size":172,"tracks":[{"file":"bbb_vcd.bin","sha1":"e7952e60928dba7f3ceb6c3316894f96ad24e175","size":12169248}]},"env":{"FAKETIME":"2000-01-01 00:00:00","FAKETIME_DONT_FAKE_MONOTONIC":"1","LD_PRELOAD":"libfaketime.so.1 sha256:7ac8a1cdff1b627548de11513fddedd75014f5b041e9ae9054a634bb04aa7982"},"frame_hash_every":150,"frames":3000,"joypad_ports":2,"observables":["frame_sha1","video_sha1","audio_sha1","state_sha1","frame_hashes"],"options":{"yabause_numthreads":"1"},"presses":[{"button":0,"frame":1300,"length":15},{"button":3,"frame":1900,"length":15}],"repetitions":2,"warmup":0},"kind":"check","migrated_from_version":1,"record_version":3,"recorded_at":"2026-10-27T05:18:19+00:00","subject":"mpeg-card-vcd-bios-jp-mpegpath-build","supersedes":null,"variants":[{"flags":[],"label":"mpeg_card.bin","repetitions":[{"env_unanswered":{"11":1,"13":1,"28":1,"35":1,"36":1,"42":1,"44":1,"49":3000,"51":1,"57":1,"8":1},"files_opened":[{"path":"sega_101.bin","sha1_after":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720","sha1_staged":"df94c5b4d47eb3cc404d88b33a8fda237eaf4720"},{"path":"mpeg_card.bin","sha1_after":"2f5ebd434f6d5b82e93d03e5626007bd5380f652","sha1_staged":"2f5ebd434f6d5b82e93d03e5626007bd5380f652"}],"files_probed_absent":[],"fps":298.7348625172993,"fps_nominal":60.0,"frames":3000,"input_pressed":30,"input_queries":78000,"last_frame_size":[320,224],"library_name":"Yabause","library_version":"v0.9.15 625bee35c","loaded":true,"logs":[{"frame":0,"level":"warn","text":"<system>/saturn_bios.bin NOT FOUND"},{"frame":0,"level":"debug","text":"Starting emulation"},{"frame":0,"level":"debug","text":"Enabling cheevos"}],"ms_per_frame":3.3474499480022737,"observables":{"audio_sha1":"8eefdabcbbb9f662de4b0d04087df5bd18ba6e45","frame_hashes":[[0,"d87e1556593b17c10facada78fe3b3a35c0f7552"],[150,"b2f9f19e66b8981bf1547e970a8a3f6637b6934e"],[300,"06d859bca35a5194fbb8b4300dab66643988629f"],[450,"b6064ea4357cf541eac46afc7f167ed7878aab1c"],[600,"d87e1556593b17c10facada78fe3b3a35c0f7552"],[750,"cbe0871893b854ec3a8ae72b5f1be8bed18a5308"],[900,"6d9c3c739167faef80b27a5e88eeefe46a395264"],[1050,"6c20cfa2ca401bbbab7a92aa4a37910b54baa70c"],[1200,"7471401fb6d263292f57f8164a5c34994485c205"],[1350,"d939ec9ce85638cb231ac6d9703b0519e484c483"],[1500,"7915940919a745bede34db6d4fc34d975894a6eb"],[1650,"8859e95bce7870e8417ff0e430a38b96d338b115"],[1800,"50283a9425d3ff49a41cfbff6c9cce958ad93a54"],[1950,"42c55858c8eLine truncated
-204
View File
@@ -1,204 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://abdess.github.io/retrobios/api/v1/schemas/core-probe-data.schema.json",
"title": "RetroBIOS core probe record data",
"description": "The sidecar of a core probe record, measurements/data/<emulator>/<id>.json.gz: every repetition in full, the raw timing values and compared values too large for the record line. With the record it rebuilds the full result.",
"type": "object",
"additionalProperties": false,
"required": [
"variants",
"timing_raw",
"group_values"
],
"properties": {
"variants": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"label",
"repetitions"
],
"properties": {
"label": {
"type": "string",
"minLength": 1
},
"repetitions": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/repetition"
}
}
}
}
},
"timing_raw": {
"type": "object",
"additionalProperties": {
"type": "array",
"minItems": 1,
"items": {
"type": "number"
}
}
},
"group_values": {
"type": "object",
"propertyNames": {
"pattern": "^[0-9a-f]{40}$"
}
}
},
"$defs": {
"sha1": {
"type": "string",
"pattern": "^[0-9a-f]{40}$"
},
"repetition": {
"type": "object",
"required": [
"loaded",
"library_name",
"logs"
],
"properties": {
"loaded": {
"type": "boolean"
},
"library_name": {
"type": "string"
},
"library_version": {
"type": "string"
},
"fps_nominal": {
"type": "number"
},
"frames": {
"type": "integer",
"minimum": 1
},
"warmup": {
"type": "integer",
"minimum": 0
},
"wall_s": {
"type": "number",
"minimum": 0
},
"ms_per_frame": {
"type": "number",
"minimum": 0
},
"fps": {
"type": "number",
"minimum": 0
},
"observables": {
"type": "object"
},
"logs": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"frame",
"level",
"text"
],
"properties": {
"frame": {
"type": "integer",
"minimum": 0
},
"level": {
"type": "string"
},
"text": {
"type": "string"
}
}
}
},
"options_declared": {
"type": "object"
},
"env_unanswered": {
"type": "object"
},
"last_frame_size": {
"type": "array",
"items": {
"type": "integer"
}
},
"tracing": {
"type": "string"
},
"input_queries": {
"type": "integer",
"minimum": 0
},
"input_pressed": {
"type": "integer",
"minimum": 0
},
"files_opened": {
"oneOf": [
{
"type": "null"
},
{
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"path",
"sha1_after",
"sha1_staged"
],
"properties": {
"path": {
"type": "string"
},
"sha1_after": {
"$ref": "#/$defs/sha1"
},
"sha1_staged": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/$defs/sha1"
}
]
}
}
}
}
]
},
"files_probed_absent": {
"oneOf": [
{
"type": "null"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
}
}
}
}
}
+165 -116
View File
@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://abdess.github.io/retrobios/api/v1/schemas/core-probe-record.schema.json",
"title": "RetroBIOS core probe record",
"description": "One line of measurements/<emulator>.jsonl: one run of scripts/core_probe.py, a libretro core executed headless, with what is needed to judge and reproduce it. The bulky rest (every repetition, core logs, raw timings) is the gzip sidecar measurements/data/<emulator>/<id>.json.gz, valid against core-probe-data.schema.json, whose sha256 is in `data`.",
"description": "One line of measurements/<emulator>.jsonl: one run of scripts/core_probe.py, a libretro core executed headless, in full, with what is needed to judge and reproduce it. Every repetition, its core logs and per-frame hashes, and the raw timing values are in the line.",
"type": "object",
"additionalProperties": false,
"required": [
@@ -18,12 +18,11 @@
"host",
"inputs",
"variants",
"flags",
"data"
"flags"
],
"properties": {
"record_version": {
"const": 2
"const": 3
},
"id": {
"type": "string",
@@ -361,13 +360,8 @@
"required": [
"label",
"system_files",
"flags",
"repetitions",
"loaded",
"observables",
"files_opened",
"files_probed_absent",
"tracing"
"flags"
],
"properties": {
"label": {
@@ -387,80 +381,18 @@
]
}
},
"repetitions": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/repetition"
}
},
"flags": {
"type": "array",
"items": {
"type": "string"
}
},
"repetitions": {
"type": "integer",
"minimum": 1,
"description": "Repetitions run. Each is in the sidecar."
},
"loaded": {
"type": "boolean",
"description": "Every repetition loaded the content."
},
"observables": {
"type": "object",
"description": "The first repetition's observables, frame_hashes left to the sidecar."
},
"files_opened": {
"oneOf": [
{
"type": "null"
},
{
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"path",
"sha1_after",
"sha1_staged"
],
"properties": {
"path": {
"type": "string"
},
"sha1_after": {
"$ref": "#/$defs/sha1"
},
"sha1_staged": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/$defs/sha1"
}
]
}
}
}
}
]
},
"files_probed_absent": {
"oneOf": [
{
"type": "null"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"tracing": {
"type": [
"string",
"null"
]
}
}
}
@@ -541,6 +473,7 @@
"type": "object",
"additionalProperties": false,
"required": [
"value",
"variants"
],
"properties": {
@@ -551,47 +484,13 @@
"items": {
"type": "string"
}
},
"value_sha1": {
"$ref": "#/$defs/sha1",
"description": "Stands for a list or object value kept in the sidecar's group_values."
}
},
"oneOf": [
{
"required": [
"value"
]
},
{
"required": [
"value_sha1"
]
}
]
}
}
}
}
}
}
},
"data": {
"type": "object",
"additionalProperties": false,
"required": [
"sha256",
"bytes"
],
"description": "The sidecar data/<emulator>/<id>.json.gz: gzip (no name, mtime 0) of canonical JSON.",
"properties": {
"sha256": {
"$ref": "#/$defs/sha256"
},
"bytes": {
"type": "integer",
"minimum": 1
}
}
}
},
"allOf": [
@@ -666,7 +565,8 @@
"mean",
"stdev",
"min",
"max"
"max",
"raw"
],
"properties": {
"median": {
@@ -684,13 +584,162 @@
},
"max": {
"type": "number"
},
"raw": {
"type": "array",
"minItems": 1,
"items": {
"type": "number"
}
}
},
"description": "Statistics over the repetitions. The raw values are in the sidecar."
"description": "Statistics over the repetitions, with the values they were computed from."
},
"slug": {
"type": "string",
"pattern": "^[a-z0-9]+(?:[-_][a-z0-9]+)*$"
},
"repetition": {
"type": "object",
"required": [
"loaded",
"library_name",
"logs"
],
"properties": {
"loaded": {
"type": "boolean"
},
"library_name": {
"type": "string"
},
"library_version": {
"type": "string"
},
"fps_nominal": {
"type": "number"
},
"frames": {
"type": "integer",
"minimum": 1
},
"warmup": {
"type": "integer",
"minimum": 0
},
"wall_s": {
"type": "number",
"minimum": 0
},
"ms_per_frame": {
"type": "number",
"minimum": 0
},
"fps": {
"type": "number",
"minimum": 0
},
"observables": {
"type": "object"
},
"logs": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"frame",
"level",
"text"
],
"properties": {
"frame": {
"type": "integer",
"minimum": 0
},
"level": {
"type": "string"
},
"text": {
"type": "string"
}
}
}
},
"options_declared": {
"type": "object"
},
"env_unanswered": {
"type": "object"
},
"last_frame_size": {
"type": "array",
"items": {
"type": "integer"
}
},
"tracing": {
"type": "string"
},
"input_queries": {
"type": "integer",
"minimum": 0
},
"input_pressed": {
"type": "integer",
"minimum": 0
},
"files_opened": {
"oneOf": [
{
"type": "null"
},
{
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"path",
"sha1_after",
"sha1_staged"
],
"properties": {
"path": {
"type": "string"
},
"sha1_after": {
"$ref": "#/$defs/sha1"
},
"sha1_staged": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/$defs/sha1"
}
]
}
}
}
}
]
},
"files_probed_absent": {
"oneOf": [
{
"type": "null"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
}
}
}
}
}
+12 -19
View File
@@ -10,10 +10,9 @@ Three modes share one runner:
Each repetition runs in its own process, under strace when it is installed,
so the files a core opened are read from the kernel rather than guessed.
`--record` appends one record per invocation to measurements/ of this
repository, carrying what is needed to reproduce it (core build, frontend
commit, host, inputs by hash). measurement_records.py splits it into a slim
line and a gzip sidecar.
`--record` appends one record per invocation, as one line of
measurements/<emulator>.jsonl in this repository, carrying what is needed to
reproduce it (core build, frontend commit, host, inputs by hash).
"""
from __future__ import annotations
@@ -771,9 +770,8 @@ def build_parser() -> argparse.ArgumentParser:
common.add_argument("--core-commit", help="commit the core was built from")
common.add_argument("--build-command", help="command and flags that built the core")
common.add_argument("--record", metavar="DIR", nargs="?", const=str(ROOT),
help="append the record to measurements/<emulator>.jsonl and write "
"its data file in measurements/data/<emulator>/ of the checkout "
"DIR (default: this one)")
help="append the record as one line of measurements/<emulator>.jsonl "
"in the checkout DIR (default: this one)")
common.add_argument("--emulator", help="profile the record is filed under (default: the "
"profile whose cores: names the .so, else the "
"core's library name)")
@@ -938,24 +936,19 @@ def default_subject(record: dict) -> str:
return f"{record['kind']}-{slugify(content, 30)}-{digest}"
def write_record(directory: Path, full: dict) -> tuple[Path, Path]:
"""Append the record to its emulator's .jsonl and write its sidecar."""
from measurement_records import ( # noqa: PLC0415
line_text, parse_lines, records_path, seal, sidecar_path, split)
def write_record(directory: Path, full: dict) -> Path:
"""Append the record, with its id, to its emulator's .jsonl."""
from measurement_records import line_text, parse_lines, records_path, seal # noqa: PLC0415
slim, sidecar = split(full)
sealed, blob = seal(slim, sidecar)
sealed = seal(full)
lines = directory / records_path(sealed["emulator"])
existing = parse_lines(lines.read_text(encoding="utf-8"), str(lines)) if lines.is_file() else []
if sealed["supersedes"] and sealed["supersedes"] not in {r["id"] for r in existing}:
raise ProbeError(f"--supersedes {sealed['supersedes']}: no such record in {lines}")
data = directory / sidecar_path(sealed["emulator"], sealed["id"])
data.parent.mkdir(parents=True, exist_ok=True)
data.write_bytes(blob)
lines.parent.mkdir(parents=True, exist_ok=True)
with lines.open("a", encoding="utf-8") as handle:
handle.write(line_text(sealed) + "\n")
return lines, data
return lines
def probe(args: argparse.Namespace) -> int:
@@ -1032,11 +1025,11 @@ def probe(args: argparse.Namespace) -> int:
record["subject"] = args.subject or default_subject(record)
if args.record:
try:
lines, data = write_record(Path(args.record), record)
lines = write_record(Path(args.record), record)
except ProbeError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 2
print(f"recorded {lines} (line {sum(1 for _ in lines.open(encoding='utf-8'))}) and {data}",
print(f"recorded {lines} (line {sum(1 for _ in lines.open(encoding='utf-8'))})",
file=sys.stderr)
print(json.dumps(record, indent=2, sort_keys=False))
return 0
+50 -163
View File
@@ -1,22 +1,17 @@
"""Core probe records as this repository stores them.
A run of core_probe.py produces one full record. It is stored as two parts:
A run of core_probe.py produces one full record, stored as one line:
measurements/<emulator>.jsonl one slim record per line, append-only
measurements/data/<emulator>/<id>.json.gz the bulky rest, named by the record id
measurements/<emulator>.jsonl one record per line, append-only
The slim record keeps what a reader needs to judge and reproduce the result:
claim, method, observable, result, statistics, host, inputs by hash, core
build and frontend commit, and per variant the values of its first
repetition. The sidecar holds every repetition in full (per-frame hashes,
core logs, declared options), the raw timing values the statistics were
computed from, and any compared value too large to sit in a line. Its
sha256 is in the record, so the line vouches for it. ``join`` puts the two
back into the full record, ``split`` is its inverse.
A line holds the whole run: claim, method, observable, result, host, inputs
by hash, core build and frontend commit, and every repetition of every
variant in full (per-frame hashes, core logs, declared options), with the
raw timing values the statistics were computed from.
A record id is the date and the first 12 hex digits of the sha256 of the
record without its id. A record carried over from the first layout keeps
the id it had, which hashes that full record.
record without its id, in canonical JSON. A record carried over from the
first layout keeps the id it had, which hashes that full record.
Standard library only: core_probe.py, validate_schemas.py and the site
read it.
@@ -24,8 +19,6 @@ read it.
from __future__ import annotations
import copy
import gzip
import hashlib
import json
import re
@@ -34,19 +27,14 @@ from collections.abc import Iterable
from dataclasses import dataclass
from pathlib import Path
RECORD_VERSION = 2
RECORD_VERSION = 3
RECORDS_DIR = "measurements"
DATA_DIR = f"{RECORDS_DIR}/data"
# Fields a repetition keeps in the slim record, per variant, from the first one.
VARIANT_SUMMARY = ("observables", "files_opened", "files_probed_absent", "tracing")
# Observables too large for a line: their values stay in the sidecar.
BULKY_OBSERVABLES = ("frame_hashes",)
ID_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}-[0-9a-f]{12}$")
SLUG_PATTERN = re.compile(r"^[a-z0-9]+(?:[-_][a-z0-9]+)*$")
class RecordError(ValueError):
"""A record or sidecar that does not match its counterpart."""
"""A record line that cannot be read."""
def canonical(value: object) -> bytes:
@@ -57,6 +45,11 @@ def sha256_hex(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def value_sha1(value: object) -> str:
"""The sha1 of a value in canonical JSON."""
return hashlib.sha1(canonical(value)).hexdigest()
def slugify(text: str, limit: int = 40) -> str:
"""Lowercase words joined by hyphens, cut at a word boundary."""
words = re.findall(r"[a-z0-9]+", text.lower())
@@ -69,151 +62,55 @@ def slugify(text: str, limit: int = 40) -> str:
return out or "record"
def record_id(slim: dict) -> str:
body = {key: value for key, value in slim.items() if key != "id"}
return f"{slim['recorded_at'][:10]}-{sha256_hex(canonical(body))[:12]}"
def record_id(record: dict) -> str:
body = {key: value for key, value in record.items() if key != "id"}
return f"{record['recorded_at'][:10]}-{sha256_hex(canonical(body))[:12]}"
def line_text(slim: dict) -> str:
def seal(record: dict) -> dict:
"""The record with its id."""
return {**record, "id": record_id(record)}
def line_text(record: dict) -> str:
"""The record as one line of its .jsonl file, keys in a fixed order."""
return json.dumps(slim, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
def sidecar_path(emulator: str, rid: str) -> str:
return f"{DATA_DIR}/{emulator}/{rid}.json.gz"
return json.dumps(record, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
def records_path(emulator: str) -> str:
return f"{RECORDS_DIR}/{emulator}.jsonl"
def sidecar_bytes(sidecar: dict) -> bytes:
"""gzip of the sidecar JSON, with no name and no time in the header."""
return gzip.compress(canonical(sidecar), compresslevel=9, mtime=0)
IDENTITY = ("id", "emulator", "subject", "supersedes", "migrated_from_version")
def read_sidecar(blob: bytes) -> dict:
return json.loads(gzip.decompress(blob))
def _value_digest(value: object) -> str:
return hashlib.sha1(canonical(value)).hexdigest()
def split(full: dict) -> tuple[dict, dict]:
"""The full record as (slim record without id and data, sidecar)."""
slim = copy.deepcopy(full)
sidecar: dict = {"variants": [], "timing_raw": {}, "group_values": {}}
variants = []
for variant in slim["variants"]:
reps = variant["repetitions"]
sidecar["variants"].append({"label": variant["label"], "repetitions": reps})
first = reps[0]
summary = {key: first.get(key) for key in VARIANT_SUMMARY}
summary["observables"] = {
key: value for key, value in (first.get("observables") or {}).items()
if key not in BULKY_OBSERVABLES
}
variants.append({
"label": variant["label"],
"system_files": variant["system_files"],
"flags": variant["flags"],
"repetitions": len(reps),
"loaded": all(rep.get("loaded") for rep in reps),
**summary,
})
slim["variants"] = variants
for name, stats in (slim.get("timing") or {}).items():
sidecar["timing_raw"][name] = stats.pop("raw")
for group in ((slim.get("check") or {}).get("result") or {}).get("groups", []):
if isinstance(group.get("value"), (list, dict)):
digest = _value_digest(group["value"])
sidecar["group_values"][digest] = group.pop("value")
group["value_sha1"] = digest
return slim, sidecar
def join(slim: dict, sidecar: dict) -> dict:
"""The full record a slim record and its sidecar were split from."""
full = copy.deepcopy(slim)
full.pop("id", None)
full.pop("data", None)
labels = [v["label"] for v in full["variants"]]
if labels != [v["label"] for v in sidecar["variants"]]:
raise RecordError(f"variants differ: record {labels}, sidecar "
f"{[v['label'] for v in sidecar['variants']]}")
full["variants"] = [
{"label": variant["label"], "system_files": variant["system_files"],
"repetitions": copy.deepcopy(stored["repetitions"]), "flags": variant["flags"]}
for variant, stored in zip(full["variants"], sidecar["variants"])
]
for name, raw in sidecar.get("timing_raw", {}).items():
full["timing"][name]["raw"] = list(raw)
for group in ((full.get("check") or {}).get("result") or {}).get("groups", []):
digest = group.pop("value_sha1", None)
if digest is not None:
group["value"] = copy.deepcopy(sidecar["group_values"][digest])
return full
def seal(slim: dict, sidecar: dict, rid: str | None = None) -> tuple[dict, bytes]:
"""The slim record with its data reference and id, and the sidecar bytes.
``rid`` keeps an id given before (a record carried over from the first
layout), otherwise the id is computed.
"""
blob = sidecar_bytes(sidecar)
sealed = {**slim, "data": {"sha256": sha256_hex(blob), "bytes": len(blob)}}
sealed["id"] = rid or record_id(sealed)
return sealed, blob
def check_pair(slim: dict, blob: bytes) -> list[str]:
"""What is wrong between a stored line and its stored sidecar."""
problems = []
data = slim.get("data") or {}
if sha256_hex(blob) != data.get("sha256"):
problems.append(f"{slim.get('id')}: sidecar sha256 {sha256_hex(blob)} != {data.get('sha256')}")
if len(blob) != data.get("bytes"):
problems.append(f"{slim.get('id')}: sidecar is {len(blob)} bytes, record says {data.get('bytes')}")
if not problems:
try:
full = join(slim, read_sidecar(blob))
except (RecordError, KeyError, OSError, ValueError) as exc:
return [f"{slim.get('id')}: record and sidecar do not join: {exc}"]
if slim.get("migrated_from_version") == 1:
digest = sha256_hex(canonical(first_layout(full)))[:12]
if str(slim.get("id", "")).rsplit("-", 1)[-1] != digest:
problems.append(f"{slim.get('id')}: id does not hash the first-layout record ({digest})")
return problems
V2_IDENTITY = ("emulator", "subject", "supersedes", "migrated_from_version")
def first_layout(full: dict) -> dict:
"""A full record as the first layout wrote it: no emulator, subject or
def first_layout(record: dict) -> dict:
"""A record as the first layout wrote it: no id, emulator, subject or
supersedes, record_version 1."""
old = {key: value for key, value in full.items() if key not in V2_IDENTITY}
old = {key: value for key, value in record.items() if key not in IDENTITY}
old["record_version"] = 1
return old
def check_line(slim: dict) -> list[str]:
def check_line(record: dict) -> list[str]:
"""The structural rules a line holds, without a JSON Schema validator."""
rid = slim.get("id", "")
rid = record.get("id", "")
problems = []
if slim.get("record_version") != RECORD_VERSION:
problems.append(f"{rid}: record_version {slim.get('record_version')} != {RECORD_VERSION}")
if record.get("record_version") != RECORD_VERSION:
problems.append(f"{rid}: record_version {record.get('record_version')} != {RECORD_VERSION}")
if not ID_PATTERN.match(str(rid)):
problems.append(f"{rid!r}: not an id of the form YYYY-MM-DD-<12 hex>")
elif str(slim.get("recorded_at", ""))[:10] != rid[:10]:
problems.append(f"{rid}: id date differs from recorded_at {slim.get('recorded_at')}")
elif slim.get("migrated_from_version") is None and record_id(slim) != rid:
problems.append(f"{rid}: id does not hash the record ({record_id(slim)})")
elif str(record.get("recorded_at", ""))[:10] != rid[:10]:
problems.append(f"{rid}: id date differs from recorded_at {record.get('recorded_at')}")
elif record.get("migrated_from_version") == 1:
digest = sha256_hex(canonical(first_layout(record)))[:12]
if not rid.endswith(f"-{digest}"):
problems.append(f"{rid}: id does not hash the first-layout record ({digest})")
elif record_id(record) != rid:
problems.append(f"{rid}: id does not hash the record ({record_id(record)})")
for key in ("emulator", "subject"):
if not SLUG_PATTERN.match(str(slim.get(key, ""))):
problems.append(f"{rid}: {key} {slim.get(key)!r} is not a lowercase slug")
if not SLUG_PATTERN.match(str(record.get(key, ""))):
problems.append(f"{rid}: {key} {record.get(key)!r} is not a lowercase slug")
return problems
@@ -262,7 +159,7 @@ class Stored:
def tree_files(root: Path) -> dict[str, Path]:
"""Repository path -> file of every record file and data file under ``root``."""
"""Repository path -> file of everything under ``root``/measurements."""
base = root / RECORDS_DIR
if not base.is_dir():
return {}
@@ -271,16 +168,14 @@ def tree_files(root: Path) -> dict[str, Path]:
def read_records(root: Path) -> list[Stored]:
"""Every record line under ``root``, each checked against its rules and data."""
"""Every record line under ``root``, each checked against its rules."""
files = tree_files(root)
data_prefix = f"{DATA_DIR}/"
problems: list[str] = []
stored: list[Stored] = []
claimed: set[str] = set()
for path in sorted(p for p in files if not p.startswith(data_prefix)):
for path in sorted(files):
emulator = path[len(RECORDS_DIR) + 1:].removesuffix(".jsonl")
if not path.endswith(".jsonl") or "/" in emulator:
problems.append(f"{path}: {RECORDS_DIR}/ holds <emulator>.jsonl files and data/ only")
problems.append(f"{path}: {RECORDS_DIR}/ holds <emulator>.jsonl files only")
continue
seen: set[str] = set()
try:
@@ -298,15 +193,7 @@ def read_records(root: Path) -> list[Stored]:
if record.get("supersedes") and record["supersedes"] not in seen:
problems.append(f"{where}: supersedes {record['supersedes']}, no earlier line of this file")
seen.add(record.get("id"))
data = sidecar_path(emulator, str(record.get("id")))
claimed.add(data)
if data not in files:
problems.append(f"{where}: {data} is missing")
else:
problems.extend(f"{where}: {p}" for p in check_pair(record, files[data].read_bytes()))
stored.append(Stored(emulator, number, record))
problems.extend(f"{path}: no record line names it"
for path in sorted(files) if path.startswith(data_prefix) and path not in claimed)
if problems:
raise MeasurementsError("records under {}:\n {}".format(root, "\n ".join(problems)))
return stored
@@ -317,7 +204,7 @@ def _git(root: Path, *args: str) -> subprocess.CompletedProcess:
def committed_files(root: Path, ref: str) -> dict[str, bytes]:
"""Path -> content of every record and data file at ``ref``."""
"""Path -> content of every file under measurements/ at ``ref``."""
listing = _git(root, "ls-tree", "-r", "-z", "--name-only", ref, "--", RECORDS_DIR)
if listing.returncode != 0:
raise MeasurementsError(f"git ls-tree {ref}: {listing.stderr.decode(errors='replace').strip()}")
@@ -331,8 +218,8 @@ def committed_files(root: Path, ref: str) -> dict[str, bytes]:
def append_only_problems(root: Path, ref: str = "HEAD") -> list[str]:
"""What the tree under ``root`` changed or removed of the lines and data
files ``ref`` holds. Lines added at the end and new files are allowed."""
"""What the tree under ``root`` changed or removed of the lines ``ref``
holds. Lines added at the end and new files are allowed."""
before, after = committed_files(root, ref), tree_files(root)
problems = []
for path, content in sorted(before.items()):
+25 -19
View File
@@ -1,8 +1,8 @@
"""The measurements the site shows, from the records under measurements/.
The records are core probe runs, one append-only .jsonl per emulator with a
gzip data file per record (measurement_records.py). From them the build
writes, never committed:
The records are core probe runs, one append-only .jsonl per emulator with one
full record per line (measurement_records.py). From them the build writes,
never committed:
public/data/measurements/index.json the compact index the explorer and
the emulator pages read, one item
@@ -39,7 +39,9 @@ HOSTS_MARKER = "<!-- measurements-hosts -->"
INDEX_PATH = Path("data") / "measurements" / "index.json"
SQLITE_PATH = Path("downloads") / "measurements.sqlite"
PER_EMULATOR_DIR = Path("downloads") / "measurements"
SQLITE_SCHEMA_VERSION = "1"
SQLITE_SCHEMA_VERSION = "2"
# Observables the variants table leaves out: one value per sampled frame.
BULKY_OBSERVABLES = ("frame_hashes",)
INDEX_SCHEMA_VERSION = 1
@@ -109,7 +111,6 @@ def index_item(entry: mr.Stored, runs: int, source: dict, profiles: dict,
"id": record["id"],
"runs": runs,
"record_url": permalink(source["repo"], source["commit"], mr.records_path(entry.emulator), entry.line),
"data_url": permalink(source["repo"], source["commit"], mr.sidecar_path(entry.emulator, record["id"])),
"sqlite": f"downloads/measurements/{entry.emulator}.sqlite",
}
@@ -164,8 +165,8 @@ SCHEMA_SQL = """
superseded INTEGER NOT NULL, core_sha256 TEXT NOT NULL REFERENCES cores(sha256),
host_id INTEGER NOT NULL REFERENCES hosts(id), load_before TEXT, load_after TEXT,
max_load REAL, frontend_commit TEXT, script_sha256 TEXT NOT NULL,
script_modified INTEGER, flags TEXT NOT NULL, data_sha256 TEXT NOT NULL,
data_bytes INTEGER NOT NULL, record_url TEXT NOT NULL, line INTEGER NOT NULL,
script_modified INTEGER, flags TEXT NOT NULL, record_url TEXT NOT NULL,
line INTEGER NOT NULL,
migrated_from_version INTEGER
);
CREATE TABLE inputs (
@@ -239,7 +240,7 @@ def write_sqlite(destination: Path, stored: list[mr.Stored], source: dict | None
"measurements_repo": (source or {}).get("repo") or "",
"measurements_commit": (source or {}).get("commit") or "",
"license": "MIT",
"scope": "core probe records: the record lines, not their data files",
"scope": "core probe records, one per line of measurements/<emulator>.jsonl",
}
connection.executemany("INSERT INTO meta VALUES (?, ?)", sorted(meta.items()))
cores = {s.record["core"]["sha256"]: s.record["core"] for s in stored}
@@ -268,13 +269,13 @@ def _insert_run(connection: sqlite3.Connection, entry: mr.Stored, source: dict,
r = entry.record
host, front, inputs = r["host"], r["frontend"], r["inputs"]
rid = r["id"]
connection.execute("INSERT INTO runs VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", (
connection.execute("INSERT INTO runs VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", (
rid, entry.emulator, r["subject"], r["kind"], r["recorded_at"], r.get("supersedes"),
int(superseded), r["core"]["sha256"], hosts[_host_key(host)],
json_text(host.get("load_before")), json_text(host.get("load_after")), host.get("max_load"),
front.get("commit"), front["script_sha256"],
None if front.get("script_modified") is None else int(front["script_modified"]),
json_text(r["flags"]), r["data"]["sha256"], r["data"]["bytes"],
json_text(r["flags"]),
permalink(source.get("repo", ""), source.get("commit", ""), mr.records_path(entry.emulator), entry.line),
entry.line, r.get("migrated_from_version")))
content = inputs.get("content") or {}
@@ -293,15 +294,21 @@ def _insert_run(connection: sqlite3.Connection, entry: mr.Stored, source: dict,
rid, check["claim"], check["method"], check["observable"], check["variable"],
result["status"], result["statement"]))
connection.executemany("INSERT INTO check_groups VALUES (?, ?, ?, ?, ?)", [
(rid, position, json_text(group["value"]) if "value" in group else None,
group.get("value_sha1"), json_text(group["variants"]))
(rid, position, json_text(group["value"]),
mr.value_sha1(group["value"]) if isinstance(group["value"], (list, dict)) else None,
json_text(group["variants"]))
for position, group in enumerate(result["groups"])])
for position, variant in enumerate(r["variants"]):
reps = variant["repetitions"]
first = reps[0]
# The first repetition stands for the variant, its per-frame hashes left out.
observables = {key: value for key, value in (first.get("observables") or {}).items()
if key not in BULKY_OBSERVABLES}
connection.execute("INSERT INTO variants VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", (
rid, position, variant["label"], variant["repetitions"], int(variant["loaded"]),
json_text(variant["flags"]), json_text(variant["observables"]),
json_text(variant["files_opened"]), json_text(variant["files_probed_absent"]),
json_text(variant["tracing"])))
rid, position, variant["label"], len(reps), int(all(rep.get("loaded") for rep in reps)),
json_text(variant["flags"]), json_text(observables),
json_text(first.get("files_opened")), json_text(first.get("files_probed_absent")),
json_text(first.get("tracing"))))
connection.executemany("INSERT INTO system_files VALUES (?, ?, ?, ?)", [
(rid, position, dest, sha1) for dest, sha1 in sorted(variant["system_files"].items())])
for metric, stats in sorted((r.get("timing") or {}).items()):
@@ -411,7 +418,7 @@ def tested_item(item: dict) -> list[str]:
+ (f", {item['flags']} flag{'s' if item['flags'] != 1 else ''} raised" if item["flags"] else "")
+ ".",
"",
f" [Full record]({item['record_url']}), [its data]({item['data_url']}), {runs}.",
f" [Full record]({item['record_url']}), {runs}.",
"",
]
return lines
@@ -503,8 +510,7 @@ def data_page_section(path: Path) -> list[str]:
"`downloads/measurements/<emulator>.sqlite` the records of one emulator, linked "
f"from its page. The [Measurements]({EXPLORER_PAGE}) page reads "
"[`data/measurements/index.json`](data/measurements/index.json). A record line is "
"valid against [`core-probe-record.schema.json`](api/v1/schemas/core-probe-record.schema.json) "
"and its data file against [`core-probe-data.schema.json`](api/v1/schemas/core-probe-data.schema.json).",
"valid against [`core-probe-record.schema.json`](api/v1/schemas/core-probe-record.schema.json).",
"",
"| Table | Columns |",
"|-------|---------|",
+8 -12
View File
@@ -594,27 +594,23 @@ def _format_errors() -> list[str]:
def _measurement_errors(root: Path | None = None) -> list[str]:
"""Every record line and data file under measurements/, against their
schemas and the rules measurement_records.read_records applies."""
"""Every record line under measurements/, against its schema and the
rules measurement_records.read_records applies."""
import measurement_records as mr # noqa: PLC0415
root = root or ROOT
record_validator = _validator("core-probe-record.schema.json")
data_validator = _validator("core-probe-data.schema.json")
validator = _validator("core-probe-record.schema.json")
out: list[str] = []
files = mr.tree_files(root)
for path in sorted(p for p in files if p.endswith(".jsonl")):
emulator = path[len(mr.RECORDS_DIR) + 1:].removesuffix(".jsonl")
for number, line in enumerate(files[path].read_text(encoding="utf-8").splitlines(), 1):
for path, file in sorted(mr.tree_files(root).items()):
if not path.endswith(".jsonl"):
continue
for number, line in enumerate(file.read_text(encoding="utf-8").splitlines(), 1):
try:
record = json.loads(line)
except json.JSONDecodeError as exc:
out.append(f"{path}:{number}: {exc}")
continue
out.extend(_errors(record_validator, record, f"{path}:{number}"))
data = mr.sidecar_path(emulator, str(record.get("id")))
if data in files:
out.extend(_errors(data_validator, mr.read_sidecar(files[data].read_bytes()), data))
out.extend(_errors(validator, record, f"{path}:{number}"))
if not out:
try:
mr.read_records(root)
+6 -16
View File
@@ -261,28 +261,18 @@ class FakeCoreTests(unittest.TestCase):
self.assertEqual(record["variants"][3]["repetitions"][0]["files_probed_absent"], ["card.bin"])
self.assertEqual(first["frames"], 3)
self.assertEqual(record["emulator"], "fake")
self.assertEqual(sorted(p.relative_to(record_dir).as_posix()
for p in record_dir.rglob("*") if p.is_file()),
sorted(["measurements/fake.jsonl",
f"measurements/data/fake/{self._only_id(record_dir)}.json.gz"]))
self.assertEqual([p.relative_to(record_dir).as_posix()
for p in record_dir.rglob("*") if p.is_file()], ["measurements/fake.jsonl"])
line = json.loads((record_dir / "measurements" / "fake.jsonl").read_text())
blob = (record_dir / "measurements" / "data" / "fake" / f"{line['id']}.json.gz").read_bytes()
self.assertEqual(measurement_records.check_line(line), [])
self.assertEqual(measurement_records.check_pair(line, blob), [])
# The line and its sidecar give back the record the run printed.
self.assertEqual(measurement_records.join(line, measurement_records.read_sidecar(blob)), record)
# The line is the record the run printed, with its id.
self.assertEqual({k: v for k, v in line.items() if k != "id"}, record)
try:
from jsonschema import Draft202012Validator
except ImportError:
return
for name, document in (("core-probe-record", line),
("core-probe-data", measurement_records.read_sidecar(blob))):
schema = json.loads((REPO_ROOT / "schemas" / f"{name}.schema.json").read_text())
self.assertEqual(list(Draft202012Validator(schema).iter_errors(document)), [], name)
@staticmethod
def _only_id(record_dir: Path) -> str:
return json.loads((record_dir / "measurements" / "fake.jsonl").read_text())["id"]
schema = json.loads((REPO_ROOT / "schemas" / "core-probe-record.schema.json").read_text())
self.assertEqual(list(Draft202012Validator(schema).iter_errors(line)), [])
def test_second_run_is_appended_beside_the_first(self) -> None:
record_dir = self.tmp / "appended"
+40 -78
View File
@@ -1,8 +1,7 @@
"""Core probe records under measurements/, and what the site makes of them.
Synthetic record trees are built under tmp/tests: records split into a line
and a sidecar, read from the tree, compiled into the index and the SQLite
files. The append-only rule is checked against a commit, two branches that
Synthetic record trees are built under tmp/tests: one full record per line,
read from the tree, compiled into the index and the SQLite files. The append-only rule is checked against a commit, two branches that
each add a line merge, and the records this repository holds are checked
against HEAD.
"""
@@ -104,11 +103,7 @@ def git(cwd: Path, *args: str) -> str:
def add_record(tree: Path, full: dict) -> dict:
slim, sidecar = mr.split(full)
sealed, blob = mr.seal(slim, sidecar)
data = tree / mr.sidecar_path(sealed["emulator"], sealed["id"])
data.parent.mkdir(parents=True, exist_ok=True)
data.write_bytes(blob)
sealed = mr.seal(full)
lines = tree / mr.records_path(sealed["emulator"])
lines.parent.mkdir(parents=True, exist_ok=True)
with lines.open("a", encoding="utf-8") as handle:
@@ -152,67 +147,41 @@ class Workspace(unittest.TestCase):
return commit_all(self.tree, "feat: first records")
class RecordSplit(unittest.TestCase):
def test_round_trip(self) -> None:
class RecordLine(unittest.TestCase):
def test_a_line_holds_the_whole_record(self) -> None:
for kind in ("check", "bench"):
full = full_record("alpha", "card", "2026-10-01T10:00:00+00:00", kind=kind)
slim, sidecar = mr.split(full)
sealed, blob = mr.seal(slim, sidecar)
sealed = mr.seal(full)
with self.subTest(kind=kind):
self.assertEqual(mr.join(sealed, mr.read_sidecar(blob)), full)
self.assertEqual({k: v for k, v in sealed.items() if k != "id"}, full)
self.assertEqual(json.loads(mr.line_text(sealed)), sealed)
self.assertEqual(mr.check_line(sealed), [])
self.assertEqual(mr.check_pair(sealed, blob), [])
self.assertNotIn("logs", json.dumps(sealed["variants"]))
self.assertNotIn("frame_hashes", json.dumps(sealed["variants"]))
self.assertIn("frame_hashes", mr.line_text(sealed))
self.assertIn("card present", mr.line_text(sealed))
self.assertNotIn("\n", mr.line_text(sealed))
def test_bulky_compared_value_moves_to_the_sidecar(self) -> None:
full = full_record("alpha", "card", "2026-10-01T10:00:00+00:00", observable="frame_hashes")
slim, sidecar = mr.split(full)
groups = slim["check"]["result"]["groups"]
self.assertTrue(all("value" not in g and len(g["value_sha1"]) == 40 for g in groups))
sealed, blob = mr.seal(slim, sidecar)
self.assertEqual(mr.join(sealed, mr.read_sidecar(blob)), full)
def test_sidecar_bytes_and_id_are_reproducible(self) -> None:
def test_id_is_reproducible(self) -> None:
full = full_record("alpha", "card", "2026-10-01T10:00:00+00:00")
one = mr.seal(*mr.split(full))
two = mr.seal(*mr.split(copy.deepcopy(full)))
self.assertEqual(one, two)
self.assertRegex(one[0]["id"], r"^2026-10-01-[0-9a-f]{12}$")
self.assertEqual(mr.seal(full), mr.seal(copy.deepcopy(full)))
self.assertRegex(mr.seal(full)["id"], r"^2026-10-01-[0-9a-f]{12}$")
def test_tampering_is_named(self) -> None:
sealed, blob = mr.seal(*mr.split(full_record("alpha", "card", "2026-10-01T10:00:00+00:00")))
self.assertTrue(mr.check_pair(sealed, blob + b"x"))
changed = {**sealed, "subject": "other"}
self.assertTrue(any("does not hash" in p for p in mr.check_line(changed)))
def test_a_campaign_line_stays_under_20_kb(self) -> None:
"""Eleven variants, two repetitions each, long logs: the line is what
a reader opens, the logs are in the sidecar."""
full = full_record("alpha", "card", "2026-10-01T10:00:00+00:00",
values=tuple(f"{i:040x}" for i in range(11)))
for variant in full["variants"]:
for rep in variant["repetitions"]:
rep["logs"] = rep["logs"] * 200
rep["observables"]["frame_hashes"] = [[k, SHA1] for k in range(0, 3000, 150)]
sealed, _blob = mr.seal(*mr.split(full))
self.assertLess(len(mr.line_text(sealed).encode()), 20_000)
sealed = mr.seal(full_record("alpha", "card", "2026-10-01T10:00:00+00:00"))
for changed in ({**sealed, "subject": "other"},
{**sealed, "variants": sealed["variants"][:1]}):
self.assertTrue(any("does not hash" in p for p in mr.check_line(changed)))
def test_first_layout_record_keeps_its_id(self) -> None:
old = full_record("alpha", "card", "2026-10-01T10:00:00+00:00")
old = mr.first_layout(old)
old = mr.first_layout(full_record("alpha", "card", "2026-10-01T10:00:00+00:00"))
rid = f"2026-10-01-{mr.sha256_hex(mr.canonical(old))[:12]}"
full = {**old, "record_version": 2, "emulator": "alpha", "subject": "card",
"supersedes": None, "migrated_from_version": 1}
sealed, blob = mr.seal(*mr.split(full), rid)
self.assertEqual(sealed["id"], rid)
self.assertEqual(mr.check_line(sealed), [])
self.assertEqual(mr.check_pair(sealed, blob), [])
self.assertEqual(mr.first_layout(mr.join(sealed, mr.read_sidecar(blob))), old)
forged = {**sealed, "id": "2026-10-01-000000000000"}
self.assertTrue(mr.check_pair(forged, blob))
line = {**old, "record_version": mr.RECORD_VERSION, "emulator": "alpha", "subject": "card",
"supersedes": None, "migrated_from_version": 1, "id": rid}
self.assertEqual(mr.check_line(line), [])
self.assertEqual(mr.first_layout(line), old)
self.assertTrue(mr.check_line({**line, "id": "2026-10-01-000000000000"}))
changed = copy.deepcopy(line)
changed["variants"][0]["repetitions"][0]["logs"] = []
self.assertTrue(any("first-layout" in p for p in mr.check_line(changed)))
class ReadAndIndex(Workspace):
@@ -222,14 +191,17 @@ class ReadAndIndex(Workspace):
self.assertEqual(mr.read_records(self.tmp / "empty"), [])
def test_a_broken_tree_is_named(self) -> None:
stray = self.tree / mr.DATA_DIR / "beta" / "2026-10-02-000000000000.json.gz"
stray = self.tree / mr.RECORDS_DIR / "data" / "beta" / "2026-10-02-000000000000.json.gz"
stray.parent.mkdir(parents=True)
stray.write_bytes(b"x")
(self.tree / mr.sidecar_path("alpha", self.bench["id"])).unlink()
lines = self.tree / mr.records_path("beta")
lines.write_text(lines.read_text() * 2)
with self.assertRaises(mr.MeasurementsError) as caught:
mr.read_records(self.tree)
message = str(caught.exception)
self.assertIn(f"{mr.sidecar_path('alpha', self.bench['id'])} is missing", message)
self.assertIn(f"{mr.DATA_DIR}/beta/2026-10-02-000000000000.json.gz: no record line names it", message)
self.assertIn(f"{mr.RECORDS_DIR}/data/beta/2026-10-02-000000000000.json.gz: "
f"{mr.RECORDS_DIR}/ holds <emulator>.jsonl files only", message)
self.assertIn(f"measurements/beta.jsonl:2: id {self.beta['id']} appears twice", message)
def test_index_shape(self) -> None:
index = self.built(self.tmp / "public").index
@@ -239,7 +211,7 @@ class ReadAndIndex(Workspace):
keys = {"emulator", "emulator_name", "profiled", "systems", "system_names", "subject", "kind",
"claim", "method", "observable", "variable", "status", "statement", "content",
"variants", "repetitions", "flags", "date", "host", "core", "frontend_commit", "id",
"runs", "record_url", "data_url", "sqlite"}
"runs", "record_url", "sqlite"}
for item in index["items"]:
self.assertEqual(set(item), keys)
alpha = {i["subject"]: i for i in index["items"] if i["emulator"] == "alpha"}
@@ -247,8 +219,6 @@ class ReadAndIndex(Workspace):
self.assertEqual(alpha["card"]["system_names"], ["Sega Saturn"])
self.assertEqual(alpha["card"]["record_url"],
f"{REPO_URL}/blob/{COMMIT}/measurements/alpha.jsonl#L1")
self.assertEqual(alpha["card"]["data_url"],
f"{REPO_URL}/blob/{COMMIT}/measurements/data/alpha/{self.first['id']}.json.gz")
self.assertEqual(alpha["speed"]["status"], "measured")
self.assertIn("ms per frame", alpha["speed"]["statement"])
self.assertIn("Test CPU", alpha["speed"]["host"])
@@ -353,19 +323,12 @@ class AppendOnly(Workspace):
self.assertEqual(mr.append_only_problems(self.tree, "HEAD"),
["measurements/alpha.jsonl: line 1 changed"])
def test_a_removed_line_or_data_file_is_named(self) -> None:
def test_a_removed_line_or_file_is_named(self) -> None:
lines = self.tree / mr.records_path("alpha")
lines.write_text(lines.read_text().splitlines()[0] + "\n")
(self.tree / mr.sidecar_path("beta", self.beta["id"])).unlink()
problems = mr.append_only_problems(self.tree, "HEAD")
self.assertIn("measurements/alpha.jsonl: line 2 removed", problems)
self.assertIn(f"{mr.sidecar_path('beta', self.beta['id'])}: removed", problems)
def test_a_rewritten_data_file_is_named(self) -> None:
data = self.tree / mr.sidecar_path("beta", self.beta["id"])
data.write_bytes(data.read_bytes() + b"x")
(self.tree / mr.records_path("beta")).unlink()
self.assertEqual(mr.append_only_problems(self.tree, "HEAD"),
[f"{mr.sidecar_path('beta', self.beta['id'])}: rewritten"])
["measurements/alpha.jsonl: line 2 removed", "measurements/beta.jsonl: removed"])
def test_two_branches_adding_a_line_merge_cleanly(self) -> None:
git(self.tree, "checkout", "-q", "-b", "left")
@@ -385,7 +348,7 @@ class AppendOnly(Workspace):
class CommittedRecords(unittest.TestCase):
"""The records this repository holds."""
def test_no_line_or_data_file_changed_or_vanished_since_head(self) -> None:
def test_no_line_changed_or_vanished_since_head(self) -> None:
if subprocess.run(["git", "-C", str(REPO_ROOT), "rev-parse", "--verify", "-q", "HEAD"],
capture_output=True, check=False).returncode != 0:
self.skipTest("not a git checkout")
@@ -413,7 +376,6 @@ class SitePages(unittest.TestCase):
"file": "x.so", "sha256": SHA256, "repo": None, "build": "make"},
"frontend_commit": None, "id": "2026-10-27-43527b763ace", "runs": 2,
"record_url": f"{REPO_URL}/blob/{'e' * 40}/measurements/beetle_saturn.jsonl#L4",
"data_url": f"{REPO_URL}/blob/{'e' * 40}/measurements/data/beetle_saturn/2026-10-27-43527b763ace.json.gz",
"sqlite": "downloads/measurements/beetle_saturn.sqlite",
}
@@ -422,7 +384,7 @@ class SitePages(unittest.TestCase):
text = "\n".join(sm.tested_section("beetle_saturn", [self.ITEM]))
self.assertTrue(text.startswith("## Tested\n"))
for expected in ("**Result:** `same`", "**Method:** Twice per card. Every input fixed.", "**Ran on:** Test CPU, 12 CPUs",
self.ITEM["record_url"], self.ITEM["data_url"], "1 flag raised", "2 runs",
self.ITEM["record_url"], "1 flag raised", "2 runs",
"../downloads/measurements/beetle_saturn.sqlite", "../wiki/measurements.md",
"&lt;image>"):
self.assertIn(expected, text)
@@ -52,7 +52,6 @@ const base = import.meta.env.BASE_URL.replace(/\/?$/, '/');
host: string;
runs: number;
record_url: string;
data_url: string;
sqlite: string;
core: { library: string; commit: string | null };
};
@@ -124,7 +123,7 @@ const base = import.meta.env.BASE_URL.replace(/\/?$/, '/');
host.append(text('span', item.host), text('small', `Core ${item.core.library}${item.core.commit ? ` at ${item.core.commit.slice(0, 12)}` : ''}`));
tr.append(host);
const record = document.createElement('td');
record.append(link(item.record_url, 'Record'), document.createTextNode(' '), link(item.data_url, 'data'));
record.append(link(item.record_url, 'Record'));
record.append(text('small', item.runs === 1 ? '1 run' : `${item.runs} runs`));
tr.append(record);
return tr;
+1 -1
View File
@@ -57,7 +57,7 @@ Every module in `tests/`, by the area it guards.
| Installer and downloads | loopback server | `test_check_release`, `test_download`, `test_install`, `test_install_cores`, `test_install_experience`, `test_install_app`, `test_install_from`, `test_install_offline`, `test_install_selection`, `test_install_tui` |
| Handheld launchers | a pty as the console, fake installer and firmware tools, stubbed PortMaster dialogs | `test_handheld_launchers`, `test_port` |
| Running cores | parsers on fixed inputs, a libretro core built from C in the test when a compiler exists | `test_core_probe` |
| Measurement records | synthetic record trees, the committed `measurements/` against HEAD | `test_measurements` |
| Measurement records | synthetic record files, the committed `measurements/` against HEAD | `test_measurements` |
| Truth and native export | synthetic + cached originals | `test_export_counts`, `test_export_native_layer`, `test_export_report`, `test_exporters`, `test_truth_data_dir_readers`, `test_truth_diff`, `test_truth_layout` |
| Scrapers, upstream and provenance | inline sources, synthetic | `test_batocera_md5_list`, `test_check_freshness`, `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_dedup`, `test_doc_pages`, `test_doc_surfaces`, `test_elec_scrapers`, `test_fbneo_parser`, `test_find_pinned_revision`, `test_hash_merge`, `test_launchers`, `test_launchers_emudeck`, `test_launchers_recalbox`, `test_launchers_retrobat`, `test_launchers_retrodeck`, `test_launchers_retropie`, `test_launchers_rocknix`, `test_mame_devices`, `test_mame_parser`, `test_mame_ref_audit`, `test_optional_dependencies`, `test_profile_sync`, `test_prose_style`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_schema_semantic_gate`, `test_scraper_contract`, `test_site_availability`, `test_site_content_data`, `test_site_copy`, `test_site_cores`, `test_site_emulator_page`, `test_site_exports`, `test_site_funnel`, `test_site_images`, `test_site_picker`, `test_site_platform_hashes`, `test_site_r2_components`, `test_site_r2_css`, `test_site_r2_emulators`, `test_site_r2_entry`, `test_site_r2_gate`, `test_site_r2_nav`, `test_site_r2_p0`, `test_site_r2_reference`, `test_site_r2_systems`, `test_site_r2_wiki`, `test_site_r3_collection`, `test_site_r3_components`, `test_site_r3_css`, `test_site_r3_emulators`, `test_site_r3_entry`, `test_site_r3_nav`, `test_site_r3_p0`, `test_site_r3_reference`, `test_site_r3_systems`, `test_site_r3_wiki`, `test_site_r4_components`, `test_site_r4_css`, `test_site_r4_emulators`, `test_site_r4_entry`, `test_site_r4_merge`, `test_site_r4_p0`, `test_site_r4_reference`, `test_site_r4_search`, `test_site_r4_systems`, `test_site_r4_wiki`, `test_site_r5_components`, `test_site_r5_css`, `test_site_r5_emulators`, `test_site_r5_entry`, `test_site_r5_reference`, `test_site_r5_search`, `test_site_r5_systems`, `test_site_r5_wiki`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_source_ref_targets`, `test_upstream`, `test_upstream_index`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` |
+18 -19
View File
@@ -745,7 +745,7 @@ strace the record says that tracing was unavailable.
| `--state-var SECTION.VAR` | a variable of a Mednafen save state, as an observable |
| `--state-dir` | keep the save state taken at the end of the first repetition |
| `--core-repo`, `--core-commit`, `--build-command` | where the core build comes from, copied into the record |
| `--record [DIR]` | append the record to `measurements/` of the checkout `DIR`, by default this one: one line of `measurements/<emulator>.jsonl` and its sidecar `measurements/data/<emulator>/<id>.json.gz`. Commit both |
| `--record [DIR]` | append the record as one line of `measurements/<emulator>.jsonl` in the checkout `DIR`, by default this one. Commit it |
| `--emulator` | the profile the record is filed under. By default the profile whose `cores:` names the `.so`, else the core's library name |
| `--subject` | a slug naming the test, shared by its reruns. By default derived from the claim and method, or from the inputs of a run or bench |
| `--supersedes ID` | the id of an earlier record of the same emulator this one corrects |
@@ -766,27 +766,26 @@ host (CPU, kernel, Python, load before and after) and each input by hash.
Core log lines arrive through the libretro log interface. Floating-point
arguments of a log line cannot be read from Python and print as `<double>`.
A record is stored in two parts by `measurement_records.py`. The line keeps
the claim, method, result, timing statistics, host, core build, frontend
commit, inputs by hash and, per variant, the first repetition's observables
and files opened, under 20 KB. The sidecar keeps every repetition in full
(per-frame hashes, core logs, declared options), the raw timing values and
any compared value too large for a line, and its sha256 is in the line. The
id is the date and the first 12 hex digits of the sha256 of the line without
its id. Schemas: `core-probe-record.schema.json` for a line,
`core-probe-data.schema.json` for a sidecar. `join` rebuilds the full record
the run printed, `check_pair` checks a line against its sidecar.
A record is stored whole, as one line (`measurement_records.py`): the claim,
method, result, host, core build, frontend commit, inputs by hash, and every
repetition of every variant in full (per-frame hashes, core logs, declared
options), with the raw timing values the statistics come from. The id is the
date and the first 12 hex digits of the sha256 of the record without its id,
in canonical JSON. Schema: `core-probe-record.schema.json`.
### Measurement records
The records live in `measurements/` of this repository:
`measurements/<emulator>.jsonl`, one record per line in the order added, and
`measurements/data/<emulator>/<id>.json.gz`, the sidecar of each. A line is
never rewritten or removed: a new run is a new line at the end of its file,
and a correction is a new line naming the record it `supersedes`. Every line
must hash to its id and join its sidecar, which `validate_schemas.py` checks
with both schemas. `tests/test_measurements.py` fails when a line or data
file that HEAD holds has changed or gone. Two branches that each add a line
The records live in `measurements/` of this repository, one file per
emulator: `measurements/<emulator>.jsonl`, one full record per line in the
order added. A line is never rewritten or removed: a new run is a new line at
the end of its file, and a correction is a new line naming the record it
`supersedes`. Every line must hash to its id and validate against the record
schema, which `validate_schemas.py` checks. `tests/test_measurements.py`
fails when a line that HEAD holds has changed or gone. The seven records of
2026-10-27 were first written one JSON file per run, then split into a line
and a gzip data file, then merged back into one line. Each keeps its first
id, which hashes the first file (`migrated_from_version: 1`), so the line
still proves it rebuilds that file. Two branches that each add a line
merge cleanly: `.gitattributes` marks `measurements/*.jsonl` `merge=union`.
`generate_site.py` compiles the records into files it never commits