summaryrefslogtreecommitdiff
path: root/source/util/StressTest.mc
diff options
context:
space:
mode:
Diffstat (limited to 'source/util/StressTest.mc')
-rw-r--r--source/util/StressTest.mc410
1 files changed, 406 insertions, 4 deletions
diff --git a/source/util/StressTest.mc b/source/util/StressTest.mc
index d1f99ea..a9d9293 100644
--- a/source/util/StressTest.mc
+++ b/source/util/StressTest.mc
@@ -1,5 +1,5 @@
// Author: cirakg & claude ai
-// Summary: Watchdog stress build: the largest code of every 2D symbology, encoded, packed, turned into runs and drawn, at twice the normal steps per event.
+// Summary: Watchdog stress build: the largest code of every 2D symbology, encoded, packed, turned into runs and drawn, at twice the normal steps per event; the PIN and change-queue work at the same margin.
//
// Two versions of this module exist; each build keeps exactly one:
// * (:no_stress), in every normal build: ON is false.
@@ -14,8 +14,13 @@
// where the runner already does two steps per tick). Codes that fit the
// screen are then drawn the way CodeView draws them (RunsPainter: straight
// from the runs, or from a bitmap painted in steps), in one onUpdate.
+// Before the last code, VaultCase items measure the vault work after a PIN
+// and on the change queue (LockJob, ChangeQueueJob, ManageRun replacing
+// 4 KB of contents or a long secret, counting and describing the queue,
+// 0.9.0 items) at the same margin.
// docker/scripts/app-diag.sh watchdog reads the DIAG stress lines.
+import Toybox.Application;
import Toybox.Graphics;
import Toybox.Lang;
import Toybox.System;
@@ -70,6 +75,362 @@ module StressTest {
}
}
+ // A RAM store for the vault cases. Values are kept as they are, without
+ // the copies Application.Storage (or the tests' MemoryBackend) makes, so
+ // the store adds no VM loops to what is measured, and two full vaults
+ // fit without the storage limits.
+ class RamBackend extends Persist.Backend {
+ var values as Dictionary<String, Application.Storage.ValueType> = {} as Dictionary<String, Application.Storage.ValueType>;
+
+ function initialize() {
+ Backend.initialize();
+ }
+
+ function getValue(key as String) as Application.Storage.ValueType {
+ return values[key] as Application.Storage.ValueType;
+ }
+
+ function setValue(key as String, value as Application.Storage.ValueType) as Void {
+ values[key] = value;
+ }
+
+ function deleteValue(key as String) as Void {
+ values.remove(key);
+ }
+ }
+
+ // One case of vault work after a PIN or on a queue change. Setup builds
+ // the vaults in small steps (one entry, queue item or KDF step each).
+ // The measured run then does the work at MARGIN: what runs in one event
+ // runs twice in it, and jobs run on two identical vaults (A and B), one
+ // step of each per step of this job, in stages (one job per vault each).
+ // * UNLOCK: each vault has a PIN, LOCKS unlocked entries with a queued
+ // "Require PIN" each, `pending` pending locked entries and a locked
+ // session. B's unlock derivation finishes during setup; A's runs in
+ // the measured run. Its last step finishes both unlocks, as
+ // PinView.onUnlockDone does; then the LockJobs run.
+ // * COUNT / DESCRIBE: one locked vault. COUNT's queue holds a REPLACE
+ // of a locked code with `material` bytes of checked contents and
+ // LOCKS - 1 LOCKs, and the work is ChangeQueue.changed() +
+ // countToConfirm(). DESCRIBE's holds LOCKS edits with names of the
+ // largest size, and the work is DescribeJob (QueueMenu), stepped on
+ // the one vault MARGIN times per step.
+ // * REPLACE_QUEUED: a locked code entry, a REPLACE with `material`
+ // digits queued by ManageRun (as the settings queue it) during
+ // setup; the measured run applies it with ChangeQueueJob.
+ // * REPLACE_DIRECT: an unlocked code entry, and the same REPLACE
+ // applied at once (ManageRun, not confirmed, as SettingsJob runs it).
+ // * LEGACY: a locked code entry and a 0.9.0 queue item (a bare box,
+ // aad "OTPW-Q-1") replacing it with `material` raw digits; locked,
+ // with both unlock derivations done in setup. Stages: DescribeJob
+ // (the menu), the unlock and LockJob, ChangeQueueJob ("Confirm all"
+ // with the menu's keys).
+ // * SECRET: a locked OTP entry, and a REPLACE with a valid Base32
+ // secret padded with spaces to ManageRun.MAX_SECRET_TEXT characters:
+ // queued (ManageRun), then confirmed (ChangeQueueJob).
+ // * SPACES: an unlocked code entry and a REPLACE of `material` spaces
+ // (refused: "Contents: missing").
+ class VaultCase extends Jobs.Job {
+ static const UNLOCK = 0;
+ static const COUNT = 1;
+ static const DESCRIBE = 2;
+ static const REPLACE_QUEUED = 3;
+ static const REPLACE_DIRECT = 4;
+ static const LEGACY = 5;
+ static const SECRET = 6;
+ static const SPACES = 7;
+ // Unlocked entries, one queued item each (ChangeQueue.MAX_ITEMS).
+ static const LOCKS = 16;
+ static const PIN = "1234";
+
+ var name as String;
+ var kind as Number;
+ var pending as Number;
+ var material as Number;
+ // False during setup, true in the measured run.
+ var measuring as Boolean = false;
+ var steps as Number = 0;
+ var measured as Number = 0;
+ var peak as Number = 0;
+ var result as String = "";
+
+ private var _vaults as Array<Vault> = [] as Array<Vault>;
+ private var _pins as Array<PinJob> = [] as Array<PinJob>;
+ private var _targets as Array<String> = [] as Array<String>;
+ private var _phase as Number = 0;
+ private var _n as Number = 0;
+ private var _job as PinJob? = null;
+ private var _setupRun as ManageRun? = null;
+ // The measured stage, its jobs (one per vault) and which are done.
+ private var _stage as Number = -1;
+ private var _jobs as Array<Jobs.Job> = [] as Array<Jobs.Job>;
+ private var _done as Array<Boolean> = [] as Array<Boolean>;
+ // LEGACY: the keys each vault's menu read.
+ private var _keys as Array<Array<String>> = [] as Array<Array<String>>;
+
+ function initialize(label as String, type as Number, pendingCount as Number, materialBytes as Number) {
+ Job.initialize();
+ name = label;
+ kind = type;
+ pending = pendingCount;
+ material = materialBytes;
+ }
+
+ function step(budget as Number) as Boolean {
+ var used = System.getSystemStats().usedMemory;
+ peak = used > peak ? used : peak;
+ if (measuring) {
+ measured++;
+ return measure(budget);
+ }
+ steps++;
+ return setup(budget);
+ }
+
+ private function vaultCount() as Number {
+ return kind == COUNT || kind == DESCRIBE ? 1 : 2;
+ }
+
+ // Entries per vault.
+ private function entryCount() as Number {
+ return kind >= REPLACE_QUEUED ? 1 : LOCKS;
+ }
+
+ // One bounded setup step; true when every vault is ready.
+ private function setup(budget as Number) as Boolean {
+ var v = _vaults.size() > 0 ? _vaults[_vaults.size() - 1] : null;
+ if (_phase == 0) {
+ v = new Vault(new RamBackend(), new TimeSource());
+ _vaults.add(v);
+ _targets = [] as Array<String>;
+ _job = v.beginSetPin(Bytes.fromUtf8(PIN), Kdf.MIN_ITERATIONS);
+ _phase = 1;
+ } else if (_phase == 1) {
+ if ((_job as PinJob).step(budget)) {
+ (v as Vault).finishSetPin(_job as PinJob);
+ _job = null;
+ _phase = 2;
+ _n = 0;
+ }
+ } else if (_phase == 2) {
+ _targets.add((v as Vault).add(newEntry()));
+ _n++;
+ if (_n == entryCount()) {
+ if (kind <= DESCRIBE || kind == LEGACY) {
+ (v as Vault).lock();
+ }
+ _phase = 3;
+ _n = 0;
+ }
+ } else if (_phase == 3) {
+ // Locked entries added while locked wait under "p." (pending).
+ if (_n < pending) {
+ (v as Vault).add(Fixtures.otp("Pending " + _n, "acct", "Stress", Entry.OTP_TOTP, Hashes.SHA1, 6, 30,
+ "12345678901234567890", true));
+ _n++;
+ } else {
+ _phase = 4;
+ _n = 0;
+ }
+ } else if (_phase == 4) {
+ if (queueStep(v as Vault)) {
+ if (kind != UNLOCK && kind != LEGACY) {
+ _phase = 0;
+ return _vaults.size() == vaultCount();
+ }
+ _pins.add((v as Vault).beginUnlock(Bytes.fromUtf8(PIN)));
+ // UNLOCK: the last vault (A) is unlocked in the measured run.
+ if (kind == UNLOCK && _vaults.size() == vaultCount()) {
+ return true;
+ }
+ _phase = 5;
+ }
+ } else if (_pins[_pins.size() - 1].step(budget)) {
+ _phase = 0;
+ return _vaults.size() == vaultCount();
+ }
+ return false;
+ }
+
+ private function newEntry() as Entry {
+ if (kind == SECRET) {
+ return Fixtures.otp("Bank", "acct", "Stress", Entry.OTP_TOTP, Hashes.SHA1, 6, 30, "12345678901234567890", true);
+ }
+ if (kind >= REPLACE_QUEUED || (kind != UNLOCK && _n == 0)) {
+ // The code a REPLACE targets; locked unless replaced directly.
+ var e = Fixtures.code("Card", Codes.QR, "1234567890");
+ e.locked = kind != REPLACE_DIRECT && kind != SPACES;
+ return e;
+ }
+ return Fixtures.otp("Stress " + _n, "acct", "Stress", Entry.OTP_TOTP, Hashes.SHA1, 6, 30, "12345678901234567890", false);
+ }
+
+ // The REPLACE of the replace cases, as ManageForm.read gives it.
+ private function replaceAction(v as Vault) as ManageAction {
+ var a = new ManageAction();
+ a.id = _targets[0];
+ a.action = ManageAction.REPLACE;
+ if (kind == SECRET) {
+ // 200 Base32 characters (125 bytes) in groups, spaces around.
+ var group = repeat(" ", 16) + "AAAA";
+ var groups = 200 / 4;
+ a.text = repeat(" ", ManageRun.MAX_SECRET_TEXT - groups * group.length()) + repeat(group, groups);
+ } else if (kind == SPACES) {
+ a.text = repeat(" ", material);
+ } else {
+ a.text = repeat("0123456789", (material + 9) / 10).substring(0, material) as String;
+ }
+ return a;
+ }
+
+ // One step of queueing; true when the vault's queue is complete.
+ private function queueStep(v as Vault) as Boolean {
+ if (kind == REPLACE_DIRECT || kind == SECRET || kind == SPACES) {
+ return true;
+ }
+ if (kind == LEGACY) {
+ // As 0.9.0 queued it: raw contents inside one box.
+ var old = new ManageAction();
+ old.id = _targets[0];
+ old.action = ManageAction.REPLACE;
+ old.label = "Card";
+ old.material = Bytes.fromUtf8(replaceAction(v).text as String);
+ var stored = ChangeQueue.items(v);
+ stored.add(SealedBox.sealKeep(v.keyManager.deviceKeys(), Bytes.fromUtf8(ChangeQueue.AAD_V1), old.toBytes(),
+ [] as Array<ByteArray>)); // setup: nothing to wipe
+ v.storage().setValue(ChangeQueue.KEY, stored);
+ ChangeQueue.changed();
+ return true;
+ }
+ if (kind == REPLACE_QUEUED) {
+ var run = _setupRun;
+ if (run == null) {
+ _setupRun = new ManageRun(v, replaceAction(v), false);
+ return false;
+ }
+ if (!run.step(1)) {
+ return false;
+ }
+ _setupRun = null;
+ if (!run.queued) {
+ result = " setup_problem=" + run.message;
+ }
+ return true;
+ }
+ var a = new ManageAction();
+ a.id = _targets[_n];
+ a.label = "Stress " + _n;
+ a.action = ManageAction.LOCK;
+ if (kind == DESCRIBE) {
+ a.action = ManageAction.EDIT;
+ a.name = repeat("N", Entry.MAX_NAME_BYTES);
+ a.account = repeat("A", Entry.MAX_NAME_BYTES);
+ a.group = repeat("G", Entry.MAX_GROUP_BYTES);
+ a.label = Forms.label(a.name, a.account);
+ } else if (kind == COUNT && _n == 0) {
+ // Checked contents, as ManageRun queues them.
+ a.action = ManageAction.REPLACE;
+ a.material = Bytes.fromUtf8(repeat("abcdefghij", (material + 9) / 10).substring(0, material) as String);
+ a.decoded = true;
+ }
+ ChangeQueue.add(v, a);
+ _n++;
+ return _n == LOCKS;
+ }
+
+ // The measured run.
+ private function measure(budget as Number) as Boolean {
+ if (kind == COUNT) {
+ for (var i = 0; i < MARGIN; i++) {
+ ChangeQueue.changed();
+ result = " to_confirm=" + ChangeQueue.countToConfirm(_vaults[0]);
+ }
+ return true;
+ }
+ if (_stage < 0) {
+ if (kind == UNLOCK) {
+ // A's derivation; its last step finishes both unlocks.
+ if (!_pins[_vaults.size() - 1].step(budget)) {
+ return false;
+ }
+ Diag.data("stress", "kdf_done name=" + name);
+ }
+ return nextStage();
+ }
+ var all = true;
+ for (var i = 0; i < _jobs.size(); i++) {
+ if (!_done[i]) {
+ _done[i] = _jobs[i].step(1);
+ all = all && _done[i];
+ }
+ }
+ if (!all) {
+ return false;
+ }
+ summary();
+ return nextStage();
+ }
+
+ // Start the next stage's jobs (A first); true when there is none.
+ private function nextStage() as Boolean {
+ _stage++;
+ var stages = kind == LEGACY ? 3 : (kind == SECRET ? 2 : 1);
+ if (_stage >= stages) {
+ return true;
+ }
+ var previous = _jobs;
+ _jobs = [] as Array<Jobs.Job>;
+ _done = [] as Array<Boolean>;
+ var last = _vaults.size() - 1;
+ for (var i = last; i >= 0; i--) {
+ var v = _vaults[i];
+ var job = new Jobs.Job(); // replaced below
+ if (kind == DESCRIBE) {
+ _jobs.add(new DescribeJob(v));
+ _done.add(false);
+ job = new DescribeJob(v);
+ } else if (kind == UNLOCK) {
+ result += " v" + i + "_unlocked=" + v.finishUnlock(_pins[i]);
+ job = new LockJob(v, true);
+ } else if (kind == REPLACE_QUEUED || (kind == SECRET && _stage == 1)) {
+ job = new ChangeQueueJob(v, null);
+ } else if (kind == LEGACY) {
+ if (_stage == 0) {
+ job = new DescribeJob(v);
+ } else if (_stage == 1) {
+ _keys.add((previous[last - i] as DescribeJob).keys);
+ result += " v" + i + "_unlocked=" + v.finishUnlock(_pins[i]);
+ job = new LockJob(v, true);
+ } else {
+ job = new ChangeQueueJob(v, _keys[last - i]);
+ }
+ } else {
+ job = new ManageRun(v, replaceAction(v), false);
+ }
+ _jobs.add(job as Jobs.Job);
+ _done.add(false);
+ }
+ return false;
+ }
+
+ private function summary() as Void {
+ var job = _jobs[0];
+ var v = _vaults[_vaults.size() - 1];
+ result += " [" + _stage + "]";
+ if (job instanceof LockJob) {
+ ChangeQueue.takeLockLines(); // as the PIN screen does when it closes
+ result += " lock_lines=" + job.lines.size() + " pending_left=" + v.pendingIds().size() + " ids=" + v.ids().size()
+ + " queue_left=" + ChangeQueue.count(v);
+ } else if (job instanceof ChangeQueueJob) {
+ result += " lines=" + job.lines.size() + " first='" + job.lines[0] + "' queue_left=" + ChangeQueue.count(v);
+ } else if (job instanceof DescribeJob) {
+ result += " described=" + job.lines.size();
+ } else if (job instanceof ManageRun) {
+ result += " failed=" + job.failed + " queued=" + job.queued + " message='" + job.message + "'";
+ }
+ }
+ }
+
// The screen: progress while encoding, the runs once an item is done.
class StressView extends WatchUi.View {
var title as String = "Stress test";
@@ -101,8 +462,9 @@ module StressTest {
}
var view as StressView? = null;
- var items as Array<Item> = [] as Array<Item>;
+ var items as Array<Item or VaultCase> = [] as Array<Item or VaultCase>;
var current as Item? = null;
+ var currentCase as VaultCase? = null;
var runner as Jobs.JobRunner? = null;
function initialView() as [WatchUi.Views] or [WatchUi.Views, WatchUi.InputDelegates] {
@@ -136,9 +498,19 @@ module StressTest {
new Item("pdf417_digits", Codes.PDF417, new Pdf417.Pdf417Job(digits.substring(0, 400) as String, 1, w, h, round)),
new Item("dm_fit", Codes.DATAMATRIX, new DataMatrix.DataMatrixJob(letters.substring(0, largestDataMatrix(w, h, round))
as String)),
+ // Single-event vault work (VaultCase).
+ new VaultCase("queue_count_big", VaultCase.COUNT, 0, Entry.MAX_PAYLOAD_BYTES),
+ new VaultCase("queue_describe", VaultCase.DESCRIBE, 0, 0),
+ new VaultCase("unlock_locks", VaultCase.UNLOCK, 0, 0),
+ new VaultCase("unlock_pending", VaultCase.UNLOCK, Vault.MAX_ENTRIES - VaultCase.LOCKS, 0),
+ new VaultCase("replace_code_4k", VaultCase.REPLACE_QUEUED, 0, Entry.MAX_PAYLOAD_BYTES),
+ new VaultCase("replace_code_4k_direct", VaultCase.REPLACE_DIRECT, 0, Entry.MAX_PAYLOAD_BYTES),
+ new VaultCase("queue_legacy_4k", VaultCase.LEGACY, 0, Entry.MAX_PAYLOAD_BYTES),
+ new VaultCase("replace_secret_1k", VaultCase.SECRET, 0, 0),
+ new VaultCase("replace_spaces_4k", VaultCase.SPACES, 0, Entry.MAX_PAYLOAD_BYTES),
// Last, so it stays on the screen: app-diag.sh decodes it.
new Item("qr_fit", Codes.QR, new Qr.QrJob(SCREEN_TEXT, 1, largestQrVersion(w, h, round), -1))
- ] as Array<Item>;
+ ] as Array<Item or VaultCase>;
Diag.data("stress", "start used_kb=" + System.getSystemStats().usedMemory / 1024);
next();
}
@@ -149,7 +521,16 @@ module StressTest {
return;
}
var item = items[0];
- items = items.slice(1, null) as Array<Item>;
+ items = items.slice(1, null) as Array<Item or VaultCase>;
+ if (item instanceof VaultCase) {
+ currentCase = item;
+ (view as StressView).title = item.name;
+ WatchUi.requestUpdate();
+ var r = new Jobs.JobRunner(item, Kdf.ITERATIONS_PER_STEP, new Lang.Method(StressTest, :onCaseReady));
+ runner = r;
+ r.start();
+ return;
+ }
current = item;
(view as StressView).title = item.name;
WatchUi.requestUpdate();
@@ -172,6 +553,27 @@ module StressTest {
r.start();
}
+ // Setup done: the measured run starts with a fresh runner, so its first
+ // event holds no setup step.
+ function onCaseReady() as Void {
+ var c = currentCase as VaultCase;
+ c.measuring = true;
+ Diag.data("stress", "start name=" + c.name + " setup_steps=" + c.steps + " used_kb=" + System.getSystemStats().usedMemory / 1024);
+ var r = new Jobs.JobRunner(c, Kdf.ITERATIONS_PER_STEP, new Lang.Method(StressTest, :onCaseDone));
+ runner = r;
+ r.start();
+ }
+
+ function onCaseDone() as Void {
+ var c = currentCase as VaultCase;
+ Diag.data("stress", "done name=" + c.name + " setup_steps=" + c.steps + " measured_steps=" + c.measured + " peak_kb=" + c.peak / 1024
+ + " total_kb=" + System.getSystemStats().totalMemory / 1024 + c.result);
+ currentCase = null;
+ var r = new Jobs.JobRunner(new Jobs.Job(), 1, new Lang.Method(StressTest, :next));
+ runner = r;
+ r.start();
+ }
+
function repeat(text as String, times as Number) as String {
var out = "";
for (var i = 0; i < times; i++) {