All files / src benchmark-workloads.js

13.79% Statements 8/58
0% Branches 0/18
0% Functions 0/13
15.38% Lines 8/52

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130            2x 2x 2x 2x     2x   2x 2x                                                                             2x                                                                                                                                                      
import { crc32, isGzip, ungzip } from "./archive.js";
import * as fdc from "./fdc.js";
import { loadData } from "./loader.js";
import { assertSnapshotFits, restoreSnapshot, snapshotFromJSON } from "./snapshot.js";
 
/** Where the workloads live, under `public/` and so served next to the page. */
export const WorkloadsDir = "benchmarks";
const ManifestFile = `${WorkloadsDir}/workloads.json`;
const DiscKeys = ["disc1", "disc2"];
const HexRadix = 16;
 
/** The checks run the restored machine this long to see its program move, then restore it again. */
const CheckCycles = 1000 * 1000;
 
const AllBits = 0xff;
const hex = (value) => `&${value.toString(HexRadix).toUpperCase()}`;
 
/**
 * Every workload's manifest entry: `name`, `model`, `fittings` (each of the Settings dialog's peripherals, true or
 * false), `description`, `source` (where its disc or program comes from), `snapshot` (the file under WorkloadsDir)
 * and `checks` (see checkWorkload).
 * @returns {Promise<object[]>}
 */
export async function listWorkloads() {
    const bytes = await loadData(ManifestFile);
    return JSON.parse(new TextDecoder().decode(bytes)).workloads;
}
 
/**
 * The named workload's manifest entry with its parsed snapshot as `snapshot`.
 * @param {string} name
 * @returns {Promise<object>}
 */
export async function readWorkload(name) {
    const workloads = await listWorkloads();
    const workload = workloads.find((candidate) => candidate.name === name);
    if (!workload) {
        const known = workloads.map((candidate) => candidate.name).join(", ");
        throw new Error(`No benchmark workload "${name}"; the workloads are ${known}`);
    }
    const bytes = await loadData(`${WorkloadsDir}/${workload.snapshot}`);
    // Vite's preview server sends a .gz file with Content-Encoding: gzip, so the browser has already inflated it.
    const json = isGzip(bytes) ? await ungzip(bytes) : bytes;
    const snapshot = snapshotFromJSON(new TextDecoder().decode(json));
    return { ...workload, snapshot };
}
 
/** Throws unless `cpu` will run on executeInternalFast, the path a normal run takes. */
export function assertFastPath(cpu) {
    if (!cpu.onFastPath) {
        throw new Error("A debug hook or tracing is on, so the CPU would run the slow executeInternal");
    }
}
 
const newDisc = ({ name, data, layout }) => fdc.discFor(name, data, undefined, layout);
 
// The snapshot names its discs as built-in ones, served from discs/, as a state saved from a default boot does.
async function readDiscImages(media = {}) {
    const images = [];
    for (const key of DiscKeys) {
        const name = media[key];
        if (!name) {
            images.push(null);
            continue;
        }
        const data = await loadData(`discs/${name}`);
        if (crc32(data) !== media[`${key}Crc32`]) {
            throw new Error(`discs/${name} is not the disc the workload was made with`);
        }
        images.push({ name, data, layout: media[`${key}Layout`] });
    }
    return images;
}
 
// A fresh disc each time, so nothing written to one carries into the next restore; a drive the snapshot
// names no disc for is emptied, whatever the page had put in it.
function restoreWithDiscs(cpu, snapshot, discImages) {
    for (const [drive, image] of discImages.entries()) cpu.fdc.loadDisc(drive, image && newDisc(image));
    restoreSnapshot(cpu, cpu.model, snapshot);
}
 
/**
 * Throws unless the machine is in the state the workload's name promises: each of `checks.bytes` holds its
 * value (under its mask, if it has one).
 * @param {import('./6502.js').Cpu6502} cpu
 * @param {object} workload
 */
export function checkWorkload(cpu, workload) {
    for (const { address, value, mask = AllBits, what } of workload.checks.bytes) {
        const found = cpu.readmem(address) & mask;
        if (found !== value) {
            throw new Error(
                `Workload "${workload.name}" is not what it says: ${what} at ${hex(address)} ` +
                    `reads ${hex(found)}, not ${hex(value)}`,
            );
        }
    }
}
 
function advancingBytes(cpu, { address, length }) {
    return Array.from({ length }, (_, offset) => cpu.readmem(address + offset)).join();
}
 
/**
 * Restores the workload into `cpu`, which must be a machine of the workload's model with no debug hooks, and
 * checks it is doing what its name says: checkWorkload, and `checks.advancing` changing over CheckCycles.
 * The machine is left freshly restored, so every caller starts from the same state.
 * @param {import('./6502.js').Cpu6502} cpu
 * @param {object} workload from readWorkload
 */
export async function restoreWorkload(cpu, workload) {
    const { snapshot } = workload;
    assertSnapshotFits(cpu, cpu.model, snapshot);
    assertFastPath(cpu);
    const discImages = await readDiscImages(snapshot.media);
    restoreWithDiscs(cpu, snapshot, discImages);
    checkWorkload(cpu, workload);
    const { advancing } = workload.checks;
    const before = advancingBytes(cpu, advancing);
    cpu.execute(CheckCycles);
    checkWorkload(cpu, workload);
    if (advancingBytes(cpu, advancing) === before) {
        throw new Error(
            `Workload "${workload.name}" is not running: ${advancing.what} at ${hex(advancing.address)} ` +
                `did not change in ${CheckCycles} cycles`,
        );
    }
    restoreWithDiscs(cpu, snapshot, discImages);
}