From b47633e7317350eff6e031c03761e2fc8a80712a Mon Sep 17 00:00:00 2001 From: Ayla Croft Date: Mon, 19 Oct 2026 19:51:55 -0400 Subject: [PATCH 1/2] Run the conformance suite on x86_64 and aarch64, held to one baseline The conformance job ran on ubuntu-latest only. The fielded edge box is aarch64, so its conformance result had never been measured. The job is now a matrix over ubuntu-latest and ubuntu-24.04-arm, with the same toolchain request on both (OTP 28, Elixir 1.18, Node 22). Both are held to the same conformance/baseline-.yml. The suite exits 1 on an unexpected failure and on a stale baseline entry, so two green jobs mean the same scenarios fail on both architectures. The results artifact is named by architecture, because one run cannot upload two artifacts with the same name. Minutes for a public repository are free on both architectures. The suite has not run on aarch64 yet; this pull request's first run is that measurement. Signed-off-by: Ayla Croft --- .github/workflows/conformance.yml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 9550bff0..f97cd0b5 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -13,6 +13,12 @@ # means nothing -- the same defect class as a check count that folds skipped checks into # passes. The number it publishes is the README's, reproducible locally by anyone with the # toolchain: `tools/conformance.sh`. +# +# TWO ARCHITECTURES, ONE BASELINE: the same job on x86_64 and on aarch64 (GitHub's arm64 +# runner, free for public repositories; the fielded edge box is aarch64). Both are held to the +# same conformance/baseline-.yml, and the suite exits 1 on an unexpected failure and +# on a stale entry, so two green jobs mean the same scenarios fail, for the same reasons, on +# both: the architectures' results are equal by construction, not by a reader comparing them. name: conformance @@ -26,8 +32,12 @@ permissions: jobs: conformance: - name: Conformance - the two rows - runs-on: ubuntu-latest + name: Conformance - the two rows, ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, ubuntu-24.04-arm] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 @@ -57,6 +67,6 @@ jobs: if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: conformance-results + name: conformance-results-${{ runner.arch }} path: ${{ runner.temp }}/conformance if-no-files-found: error From b8bb54fb7ca583984140bd097e0e2e1225d2d00b Mon Sep 17 00:00:00 2001 From: Ayla Croft Date: Mon, 19 Oct 2026 20:29:43 -0400 Subject: [PATCH 2/2] Wait for trace delivery before stopping the tracer in its tests The floor leg of gate run 39344186410 failed tracer_test.exs:913: two calls into a traced module counted as one (weight 1, want 2). The test's barrier before Tracer.stop/0 was :sys.get_state(Tracer), which waits only for messages already in the tracer's mailbox. Trace messages can still be on their way: erlang:trace_delivered/1's documentation says their delivery "is dislocated on the time-line compared to other events", and names trace_delivered as the way to wait for them. One trace message arrived after the stop and was discarded. handled/0 calls :erlang.trace_delivered(:all), waits for its reply (with a 5 s bound), then calls :sys.get_state(Tracer). The seven tests that stop the tracer after a barrier use it. trace:delivered/2 is the same BIF for a session, so the tracer's private session handle is not needed. The red was not reproduced locally (25 seeds at a 2-core cap, no failure). After the change: 10 seeds without a failure, and the full suite passes (11 properties, 767 tests, 0 failures). Signed-off-by: Ayla Croft --- test/beam_mcp/connectome/tracer_test.exs | 36 ++++++++++++++++++------ 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/test/beam_mcp/connectome/tracer_test.exs b/test/beam_mcp/connectome/tracer_test.exs index 2c40c650..964e8f86 100644 --- a/test/beam_mcp/connectome/tracer_test.exs +++ b/test/beam_mcp/connectome/tracer_test.exs @@ -47,6 +47,26 @@ defmodule BeamMCP.Connectome.TracerTest do Tracer.start(Keyword.merge([collector: collector, server: @server], opts)) end + # The barrier before a stop: every trace message the BEAM will deliver for what has run so + # far is in the tracer's mailbox, and the tracer has handled it. A `:sys.get_state/1` call + # alone waits only for what has already ARRIVED: trace delivery is "dislocated on the + # time-line compared to other events" (`erlang:trace_delivered/1`'s documentation), and the + # floor leg of gate run 39344186410 counted one of two calls (weight 1, want 2). + # `trace:delivered/2` is this same BIF for a session (kernel's trace.erl calls it), so the + # tracer's private session handle is not needed here. + defp handled do + ref = :erlang.trace_delivered(:all) + + receive do + {:trace_delivered, :all, ^ref} -> :ok + after + 5_000 -> flunk("the BEAM did not confirm trace delivery within 5 s") + end + + _ = :sys.get_state(Tracer) + :ok + end + # The tracer's sessions alive in the node, as weak handles; nothing left behind is `[]`. defp sessions, do: Enum.filter(:trace.session_info(:all), &match?({@session, _}, &1)) @@ -866,7 +886,7 @@ defmodule BeamMCP.Connectome.TracerTest do # A call that is a message to the code server from this process, whether or not the # module is already loaded (under cover every module is). _ = :code.get_path() - _ = :sys.get_state(Tracer) + :ok = handled() :ok = Tracer.stop() Process.unregister(:tracer_test_sender6) {:ok, g} = Observed.snapshot(c) @@ -884,7 +904,7 @@ defmodule BeamMCP.Connectome.TracerTest do Traced.wrapped(@marker) # The barrier: a stop ends the tracer on the next message it handles, writing nothing # after the flag, so the trace message must be handled before the stop is asked. - _ = :sys.get_state(Tracer) + :ok = handled() :ok = Tracer.stop() {:ok, %Graph{} = g} = Observed.snapshot(c) @@ -901,7 +921,7 @@ defmodule BeamMCP.Connectome.TracerTest do test "a dynamic call through apply/3 is the same edge as a static one", %{collector: c} do {:ok, _} = start(c, modules: [Beta]) Traced.apply_wrapped(Beta, [@marker]) - _ = :sys.get_state(Tracer) + :ok = handled() :ok = Tracer.stop() {:ok, g} = Observed.snapshot(c) @@ -919,7 +939,7 @@ defmodule BeamMCP.Connectome.TracerTest do {:ok, _} = start(c, modules: [Beta]) Traced.tail(1) Alpha.run(1) - _ = :sys.get_state(Tracer) + :ok = handled() :ok = Tracer.stop() {:ok, g} = Observed.snapshot(c) @@ -962,8 +982,8 @@ defmodule BeamMCP.Connectome.TracerTest do assert_receive :got assert_receive :got # stop/0 ends the tracer on the next message it handles and discards what is still - # queued; a system message is answered in mailbox order, so this waits for both. - _ = :sys.get_state(Tracer) + # queued; handled/0 waits for both trace messages to arrive and be handled first. + :ok = handled() :ok = Tracer.stop() send(receiver, :done) Process.unregister(:tracer_test_sender) @@ -1023,7 +1043,7 @@ defmodule BeamMCP.Connectome.TracerTest do send(pid, :something_else) send({:tracer_test_sender3, node()}, :to_myself_by_name) assert Tracer.running?() - _ = :sys.get_state(Tracer) + :ok = handled() :ok = Tracer.stop() Process.unregister(:tracer_test_sender3) @@ -1040,7 +1060,7 @@ defmodule BeamMCP.Connectome.TracerTest do anon = spawn_link(fn -> receive do: (_ -> :ok) end) {:ok, _} = start(c, processes: [:tracer_test_sender2]) send(anon, @marker) - _ = :sys.get_state(Tracer) + :ok = handled() :ok = Tracer.stop() Process.unregister(:tracer_test_sender2)