Skip to content

Add --clock-mhz: report emulated time from ReadEClock (issue #102) - #103

Merged
sidick merged 2 commits into
mainfrom
clock-mhz-issue-102
Sep 29, 2026
Merged

sidick merged 2 commits into
mainfrom
clock-mhz-issue-102

Conversation

@sidick

@sidick sidick commented Sep 29, 2026

Copy link
Copy Markdown
Owner

Closes #102.

Adds --clock-mhz N, which makes ReadEClock report emulated time derived
from the CPU's emulated cycle count at the given clock rate, instead of host
wall-clock time. The motivation (from @codewiz in #102) is compiler A/B
benchmarking: host time measures how fast the host ran the emulation, not what
the guest would have cost, and the distortion is per-workload — 7x to 27x
across BenchWork — so it doesn't cancel out. With this flag the numbers become
reproducible and independent of host load.

How it works

Cpu::run for M68kCpu normally goes through m68k::CpuCore::run_batch,
whose BatchResult carries only instructions — there is no cycle count on
that path at all. When --clock-mhz is given, the run loop instead uses
run_for_cycles, which returns a real cycles total. That is the
transaction-exact path (set_precise_bus(true), one instruction at a time, no
trace JIT, no fast_mem), so it is deliberately the slowest mode volamos has.

ReadEClock's reported rate in D0 stays ECLOCK_PAL_HZ (709379)
regardless of --clock-mhz. Real Amigas derive the E-clock from chipset
timing, not the CPU clock, so the flag governs only the cycles-to-time
conversion feeding ev_hi/ev_lo. GetSysTime/TR_GETSYSTIME/DateStamp/
CurrentTime all stay on the host clock — a benchmark run shouldn't think
it's 1978.

The change is strictly additive: with the flag absent, run() takes the
existing run_batch path unchanged and read_eclock_handler runs the
identical host-clock arithmetic as before.

Refused combinations

Both are hard errors rather than warnings, because in each case the result
would be silently meaningless rather than merely degraded:

  • --jit — run_batch never tracks a cycle count, so there is nothing to
    derive emulated time from. Not approximate: absent.
  • --sanitize — run_via_cycles doesn't run the per-instruction
    shadow-map hooks, so the shadow call stack, return-address checks, below-SP
    tracking and violation-PC attribution would all be dead or stale. A
    sanitizer that reports clean because its checks didn't run is worse than one
    that refuses to start.

Measured cost

CoreMark 1.0 (coremark.amiga, --cpu 68020), host throughput, 3 runs
averaged per mode: --jit ~680 iter/s, --no-jit ~217, --clock-mhz 25 ~97.
So about 2.2x slower than --no-jit and 7x slower than --jit. For
replacing a 20-minute Copperline A/B with one command that's a rounding error,
but it does mean this wants to stay opt-in for benchmarking rather than
something the testsuite picks up.

Documented caveats

userdocs/CLI-Reference.md spells out four things that stay unrepresentative,
because each could otherwise produce a confidently wrong number:

  1. No bus wait states. FlatMemory implements no sync and no bus
    timing, while Copperline bills real Chip/Fast RAM contention. Memory-bound
    code will improve enormously over host time but will not converge on real
    hardware.
  2. Native library handlers are free. Time spent in volamos's Rust
    handlers costs zero emulated cycles, so a workload dominated by e.g.
    CopyMem reports an emulated time implying it spent none there.
  3. Cpu::step doesn't accumulate. Guest code reached via step —
    RawDoFmt's PutChProc callouts, Supervisor routines — is invisible in
    the total.
  4. Nested System()/Execute() gets a fresh counter, so bracketing a
    child process reads as ~zero elapsed time.

Testing

cargo test --workspace: 1035 passed, 0 failed. Build, clippy
(--all-targets) and fmt --check all clean.

New tests cover the cycle accounting, D0 staying at the PAL rate under the
new mode, a two-reading delta that genuinely discriminates cycle-derived from
wall-clock-derived timing, the default path being unaffected, both refused
combinations in both orders, and flag parsing including fractional, zero,
negative, NaN, inf, and out-of-range values.

The cycles-to-ticks arithmetic was verified independently against a
hand-built fixture at three clock rates (25 / 12.5 / 50 MHz), producing exact
predicted tick values with correct inverse-linear scaling.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EHvk3UiKvgnG2xb71UJ67R

sidick and others added 2 commits September 29, 2026 09:34
Makes timer.device's ReadEClock report time derived from the CPU's own
emulated m68k cycle count at a configurable rate, instead of host
wall-clock time, so compiler A/B benchmarks under volamos are
reproducible and independent of host load.

- Cpu trait gains emulated_cycles()/clock_hz() with non-breaking
  defaults (0 / None).
- M68kCpu::set_clock_mhz(Some(mhz)) switches Cpu::run from run_batch to
  m68k::CpuCore::run_for_cycles, accumulating real cycle counts; the
  run_batch path is untouched when the flag is absent.
- read_eclock_handler derives ev_hi/ev_lo from cycles/clock_hz when
  active, but always reports ECLOCK_PAL_HZ in D0 -- real hardware
  derives the E-Clock from chipset timing, not the CPU clock.
  GetSysTime/TR_GETSYSTIME/DateStamp/CurrentTime are untouched.
- --clock-mhz errors cleanly when combined with an explicit --jit (the
  trace JIT never tracks a cycle count); a default-off --jit does not
  spuriously trip this, since jit only becomes true via an explicit
  override.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EHvk3UiKvgnG2xb71UJ67R
…102)

Code review fixes on the --clock-mhz branch:

1. Hard-error --clock-mhz + --sanitize (check_clock_mhz_sanitize): the
   cycle-counted run_via_cycles path never calls the sanitizer's
   per-instruction hooks, so combining the two would leave the shadow
   call stack/below-SP tracking silently stale while --sanitize still
   reports a normal-looking summary. Refused the same way as --jit,
   checked second so --jit's conflict still reports first when both
   apply.

2. Re-measured the perf claim: today's real numbers (CoreMark 1.0,
   --cpu 68020, host wall-clock throughput, three-run average) are
   ~217 iter/sec for --no-jit, ~680 for --jit, ~97 for --clock-mhz 25
   -- --clock-mhz is ~2.2x slower than --no-jit and ~7x slower than
   --jit, not the stale/wrong "~5x slower than --no-jit" previously
   quoted in three places (that figure was actually --jit-vs-interpreter,
   and pre-dated a FlatMemory read speedup that also sped up the
   precise-bus path).

3. Fixed cpu.rs's emulated_cycles doc: only Cpu::run accumulates
   cycles, never Cpu::step -- guest code reached via RawDoFmt's
   PutChProc callback or Supervisor's single-stepped routine
   (execfmt.rs) costs zero emulated cycles, same class of invisibility
   as native library handlers. Added the same caveat to
   CLI-Reference.md.

4. Documented the nested System()/Execute() cycle-counter-resets-to-zero
   caveat in CLI-Reference.md (was previously only a source comment).

5. Added MIN_CLOCK_MHZ (1 Hz) to parse_clock_mhz: --clock-mhz 1e-30
   drove ReadEClock's tick count to f64::INFINITY, saturating to
   u64::MAX on the cast -- harmless, but silently wrong.

6. check_clock_mhz_jit's error message now names --no-jit/the config
   file instead of "drop --jit" when jit came from a config file's
   JIT=true rather than an explicit CLI flag (JitSource, threaded
   through resolve()).

The "incidental pre-existing bug" reported in the previous commit is
retracted per review (reproduced by an accidental jsr-displacement typo
in the test fixture, not a real runtime bug) and not filed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EHvk3UiKvgnG2xb71UJ67R
@sidick
sidick merged commit 518a92a into main Sep 29, 2026
9 checks passed
@sidick
sidick deleted the clock-mhz-issue-102 branch September 29, 2026 09:16
sidick added a commit that referenced this pull request Sep 29, 2026
As merged in #103, volamos's cycle counter was only reachable
indirectly: the guest itself had to call ReadEClock and report its own
elapsed time. That works for an instrumented benchmark like BenchWork,
but leaves an uninstrumented binary unmeasurable -- while `vamos -v` has
printed an equivalent "total cycles:" line for its own runs all along
(amitools/vamos/main.py, summing Musashi's per-call cycle returns).

Adds Runtime::emulated_cycles(), reporting the top-level run's count and
the rate it was counted at, and prints one line from it at exit when
--clock-mhz is active. Nothing is printed otherwise, because on the
default run_batch path no cycle count exists to print.

The line goes to stderr, so a harness parsing the guest's own stdout
never sees it, and is printed even when the run ends in an error -- a
benchmark that died part-way still burned the cycles it burned.

Cross-checked against vamos on the same binary: 340,020 cycles vs
vamos's 339,044 at 68000 (+0.3%), and 157,436 vs 155,855 at 68020
(+1.0%) -- two independent cycle models (the m68k crate's timing tables
vs Musashi's) agreeing closely.


Claude-Session: https://claude.ai/code/session_01EHvk3UiKvgnG2xb71UJ67R

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sidick sidick mentioned this pull request Oct 10, 2026
2 of 3 tasks
sidick added a commit that referenced this pull request Oct 10, 2026
* Bump version to 0.9.0

Closes out the 0.9 changelog section: the ten commits merged since
v0.8 (#100) were landing under a stale "## 0.8" header without a
version bump, so this also backfills changelog entries that were
missing entirely -- the --clock-mhz base feature and its two follow-on
reports (#102/#103, #104, #107), both math-library condition-code
fixes (#112, #113), HUNK_RELRELOC32 loader support (#116), and the
dispatch task-struct-corruption diagnostic -- alongside the two that
already had entries (clap migration #108, native-handler counts #110).

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* docs: fix two stale cross-page anchor links

mkdocs slugifies a heading like "## \`--sanitize\`" to #-sanitize, not
#--sanitize -- the backtick-wrapped flag name collapses the doubled
dash to one. Found via mkdocs build --strict's anchor-mismatch INFO
output while checking 0.9's docs were current.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

For benchmarking: --clock-mhz=N, report emulated time from ReadEClock()

1 participant