diff options
Diffstat (limited to 'source/util/StressTest.mc')
| -rw-r--r-- | source/util/StressTest.mc | 410 |
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++) { |
