Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/verb-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -3569,7 +3569,7 @@ Write the whole experiment to a file, its definition beside every cell's raw num
|---|---|---|---|
| `path` | string | optional, primary | where to write it; empty writes meshbench-experiment.json in the system temporary directory, and a path that cannot be written is refused |

**Answers** `path`, `bytes`. JSON, holding the arms, seeds, senders and timings alongside `results`, which is every cell including the per-second histogram and the at-risk shares, and `summary`, which is the means. It will write an empty one before anything has run.
**Answers** `path`, `bytes`. JSON, holding the arms, seeds, senders and timings alongside `results`, which is every cell including the per-second histogram, the at-risk shares and `chip`, what each node's radio counted over the cell and how it was set at the end, and `summary`, which is the means. It will write an empty one before anything has run.

**Example** - keep the sweep, wherever the machine puts temporary files

Expand Down
2 changes: 1 addition & 1 deletion docs/verbs.json
Original file line number Diff line number Diff line change
Expand Up @@ -1450,7 +1450,7 @@
"path",
"bytes"
],
"answers": "JSON, holding the arms, seeds, senders and timings alongside `results`, which is every cell including the per-second histogram and the at-risk shares, and `summary`, which is the means. It will write an empty one before anything has run.",
"answers": "JSON, holding the arms, seeds, senders and timings alongside `results`, which is every cell including the per-second histogram, the at-risk shares and `chip`, what each node's radio counted over the cell and how it was set at the end, and `summary`, which is the means. It will write an empty one before anything has run.",
"example": {
"params": {},
"what": "keep the sweep, wherever the machine puts temporary files",
Expand Down
2 changes: 2 additions & 0 deletions internal/app/session/experiment/cell.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ func runArm(ctx context.Context, s *session.Sim, e *experiment, arm session.ExpA
// boot chatter above are receptions too, and a cell that inherited them
// would report the mesh coming up rather than the flood it measured.
eng.ResetSensitivity()
chipBefore := chipBaseline(eng)
burstMs := uint32(0)

// Sends waiting for their moment, when the senders are staggered.
Expand Down Expand Up @@ -443,6 +444,7 @@ func runArm(ctx context.Context, s *session.Sim, e *experiment, arm session.ExpA
for i := range engine.MarginEdgesDB {
out.AtRisk[i] = sens.AtRisk(i)
}
out.Chip = chipStats(eng, chipBefore)
return out
}

Expand Down
114 changes: 114 additions & 0 deletions internal/app/session/experiment/chipstats.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package experiment

import (
"github.com/MeshBench/meshbench/internal/firmware"
"github.com/MeshBench/meshbench/internal/sim/engine"
)

// ChipStats is what each node's chip saw during a cell, summed and per node.
//
// Delivery totals say what the mesh did; these say what the firmware was told.
// They are the only way to tell a busy channel from a radio that cries busy,
// and - on a chip that applies IrqMask - a firmware that never enabled an
// interrupt from one that never had cause to see it. Two builds whose radio
// drivers differ and whose totals agree are not shown to behave alike until
// these agree too.
type ChipStats struct {
IRQReads uint64 `json:"irq_reads"`
BusyReads uint64 `json:"busy_reads"`
BusyMs uint64 `json:"busy_ms"`
Spurious uint64 `json:"spurious"`
IRQSuppressed uint64 `json:"irq_suppressed"`
// Reporting is how many nodes sent a suppressed count at all. A chip that
// ignores the mask sends none, and its total of zero means unknown.
Reporting int `json:"suppressed_reporting"`
Nodes []NodeChip `json:"nodes"`
}

// NodeChip is what one node's chip counted over the cell, and how it was
// set when the cell ended.
type NodeChip struct {
Name string `json:"name"`
Kind string `json:"kind"`
IRQReads uint32 `json:"irq_reads"`
BusyReads uint32 `json:"busy_reads"`
BusyMs uint32 `json:"busy_ms"`
Spurious uint32 `json:"spurious"`
IRQSuppressed uint32 `json:"irq_suppressed"`
Reported bool `json:"suppressed_reported"`
IRQMask uint16 `json:"irq_mask"`
SF uint8 `json:"sf"`
BandwidthHz uint32 `json:"bandwidth_hz"`
}

// chipBaseline is every running node's chip as the cell's own traffic begins.
//
// The chip counts from boot, and the cell's other numbers do not: the ledger
// and the margin counters both start after the adverts have settled. Without
// this, the counters would include the mesh coming up while the delivery
// totals beside them do not, and two arms that booted differently would look
// as though they flooded differently.
func chipBaseline(eng *engine.Engine) map[string]firmware.RadioStats {
out := map[string]firmware.RadioStats{}
for _, n := range eng.Nodes() {
if n.Firmware != nil {
out[n.Spec().Name] = n.Firmware.Bridge.Stats()
}
}
return out
}

// chipStats reads every running node's chip since before. Called before the
// engine closes, while the bridges still hold the last record each node sent.
func chipStats(eng *engine.Engine, before map[string]firmware.RadioStats) *ChipStats {
out := &ChipStats{}
for _, n := range eng.Nodes() {
if n.Firmware == nil {
continue
}
name := n.Spec().Name
out.add(name, string(n.Spec().Kind), since(n.Firmware.Bridge.Stats(), before[name]))
}
return out
}

// since is end with its counters measured from start. The mask, the modem
// settings and whether a suppressed count is kept are what the chip holds at
// the end, not counts, so they are end's alone.
func since(end, start firmware.RadioStats) firmware.RadioStats {
end.IRQReads = counted(end.IRQReads, start.IRQReads)
end.BusyReads = counted(end.BusyReads, start.BusyReads)
end.BusyMs = counted(end.BusyMs, start.BusyMs)
end.SpuriousUp = counted(end.SpuriousUp, start.SpuriousUp)
end.IRQSuppressed = counted(end.IRQSuppressed, start.IRQSuppressed)
return end
}

// counted is how far a counter moved. One that went backwards was restarted
// with its node, so everything it holds was counted after the baseline;
// subtracting would wrap to four billion.
func counted(end, start uint32) uint32 {
if end < start {
return end
}
return end - start
}

// add folds one node's radio record into the cell's counters.
func (c *ChipStats) add(name, kind string, r firmware.RadioStats) {
c.IRQReads += uint64(r.IRQReads)
c.BusyReads += uint64(r.BusyReads)
c.BusyMs += uint64(r.BusyMs)
c.Spurious += uint64(r.SpuriousUp)
if r.SuppressedReported {
c.IRQSuppressed += uint64(r.IRQSuppressed)
c.Reporting++
}
c.Nodes = append(c.Nodes, NodeChip{
Name: name, Kind: kind,
IRQReads: r.IRQReads, BusyReads: r.BusyReads, BusyMs: r.BusyMs,
Spurious: r.SpuriousUp, IRQSuppressed: r.IRQSuppressed,
Reported: r.SuppressedReported, IRQMask: r.IRQMask,
SF: r.SF, BandwidthHz: r.BandwidthHz,
})
}
95 changes: 95 additions & 0 deletions internal/app/session/experiment/chipstats_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package experiment

import (
"encoding/json"
"testing"

"github.com/MeshBench/meshbench/internal/firmware"
)

// A cell sums every node's radio counters and keeps one row per node, so two
// arms whose delivery totals agree can still be told apart by what each
// firmware was told.
func TestACellSumsEveryNodesChipAndKeepsEachRow(t *testing.T) {
var c ChipStats
c.add("A", "companion", firmware.RadioStats{
IRQReads: 100, BusyReads: 10, BusyMs: 500, IRQMask: 0x0272,
IRQSuppressed: 7, SuppressedReported: true, SF: 8, BandwidthHz: 62500,
})
c.add("B", "simple-repeater", firmware.RadioStats{
IRQReads: 50, BusyReads: 5, BusyMs: 250, IRQMask: 0x0276,
IRQSuppressed: 3, SuppressedReported: true, SF: 8, BandwidthHz: 62500,
})
if c.IRQReads != 150 || c.BusyReads != 15 || c.BusyMs != 750 {
t.Errorf("sums: reads %d busy %d busy_ms %d, want 150 15 750", c.IRQReads, c.BusyReads, c.BusyMs)
}
if c.IRQSuppressed != 10 || c.Reporting != 2 {
t.Errorf("suppressed %d from %d nodes, want 10 from 2", c.IRQSuppressed, c.Reporting)
}
if len(c.Nodes) != 2 || c.Nodes[1].Name != "B" || c.Nodes[1].IRQMask != 0x0276 {
t.Errorf("per-node rows: %+v", c.Nodes)
}
}

// A chip that does not count suppressed events adds nothing to the total and
// is not counted as reporting, so a total of zero is not mistaken for a count
// of zero.
func TestAChipThatDoesNotCountIsNotCountedAsReporting(t *testing.T) {
var c ChipStats
c.add("old", "simple-repeater", firmware.RadioStats{IRQReads: 20, IRQSuppressed: 9})
if c.IRQSuppressed != 0 || c.Reporting != 0 {
t.Errorf("suppressed %d from %d nodes, want 0 from 0", c.IRQSuppressed, c.Reporting)
}
if c.Nodes[0].Reported {
t.Error("the row says the chip reported a count it does not keep")
}
}

// The counters reach the export under "chip", beside the cell's other numbers.
func TestTheExportCarriesEachCellsChipCounters(t *testing.T) {
r := Result{Arm: "a", Chip: &ChipStats{IRQReads: 1, Nodes: []NodeChip{{Name: "A"}}}}
raw, err := json.Marshal(r)
if err != nil {
t.Fatal(err)
}
var back map[string]any
if err := json.Unmarshal(raw, &back); err != nil {
t.Fatal(err)
}
chip, ok := back["chip"].(map[string]any)
if !ok || chip["irq_reads"] != float64(1) {
t.Errorf("export row: %s", raw)
}
}

// What the chip counted while the mesh came up stays out of the cell, as the
// boot chatter stays out of its delivery totals; what the chip holds at the end
// is reported as it stands.
func TestACellCountsTheChipFromItsBaselineNotFromBoot(t *testing.T) {
start := firmware.RadioStats{
IRQReads: 40, BusyReads: 4, BusyMs: 200, SpuriousUp: 1, IRQSuppressed: 6,
SuppressedReported: true, IRQMask: 0x0272, SF: 8, BandwidthHz: 62500,
}
end := firmware.RadioStats{
IRQReads: 100, BusyReads: 10, BusyMs: 500, SpuriousUp: 3, IRQSuppressed: 9,
SuppressedReported: true, IRQMask: 0x0276, SF: 8, BandwidthHz: 62500,
}
got := since(end, start)
if got.IRQReads != 60 || got.BusyReads != 6 || got.BusyMs != 300 ||
got.SpuriousUp != 2 || got.IRQSuppressed != 3 {
t.Errorf("counters %+v, want 60 6 300 2 3 counted from the baseline", got)
}
if got.IRQMask != 0x0276 || got.SF != 8 || got.BandwidthHz != 62500 || !got.SuppressedReported {
t.Errorf("state %+v, want the chip as it ended", got)
}
}

// A node that restarted mid-cell starts its counters again from zero, so
// everything it holds came after the baseline. Subtracting the larger figure
// from before would wrap to four billion.
func TestANodeThatRestartedMidCellReportsWhatItCountedSince(t *testing.T) {
got := since(firmware.RadioStats{IRQReads: 15}, firmware.RadioStats{IRQReads: 40})
if got.IRQReads != 15 {
t.Errorf("irq_reads %d, want 15", got.IRQReads)
}
}
4 changes: 4 additions & 0 deletions internal/app/session/experiment/matrix.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ type Result struct {
// actually made, so it needs no second arm to compare against and no
// assumption about what boosted gain is worth.
AtRisk []float64 `json:"at_risk,omitempty"`

// Chip is what every node's radio counted over the cell, from the same
// baseline as the totals above.
Chip *ChipStats `json:"chip,omitempty"`
}

// BuildRef identifies one firmware a cell ran: the version the arm asked for
Expand Down
2 changes: 1 addition & 1 deletion internal/app/session/experiment/results.verbs.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"path",
"bytes"
],
"answers": "JSON, holding the arms, seeds, senders and timings alongside `results`, which is every cell including the per-second histogram and the at-risk shares, and `summary`, which is the means. It will write an empty one before anything has run.",
"answers": "JSON, holding the arms, seeds, senders and timings alongside `results`, which is every cell including the per-second histogram, the at-risk shares and `chip`, what each node's radio counted over the cell and how it was set at the end, and `summary`, which is the means. It will write an empty one before anything has run.",
"example": {
"params": {},
"what": "keep the sweep, wherever the machine puts temporary files",
Expand Down
Loading