diff options
Diffstat (limited to 'source')
| -rw-r--r-- | source/app/OtpWalletApp.mc | 3 | ||||
| -rw-r--r-- | source/util/StressTest.mc | 215 |
2 files changed, 218 insertions, 0 deletions
diff --git a/source/app/OtpWalletApp.mc b/source/app/OtpWalletApp.mc index 596ccc2..3fa42de 100644 --- a/source/app/OtpWalletApp.mc +++ b/source/app/OtpWalletApp.mc @@ -44,6 +44,9 @@ class OtpWalletApp extends Application.AppBase { // The list, or a full-screen message when the data can't be used. (:typecheck(disableGlanceCheck)) function getInitialView() as [Views] or [Views, InputDelegates] { + if (StressTest.ON) { + return StressTest.initialView(); // `make watchdog` only + } _started = true; AppState.start(); var error = AppState.startError; diff --git a/source/util/StressTest.mc b/source/util/StressTest.mc new file mode 100644 index 0000000..d1f99ea --- /dev/null +++ b/source/util/StressTest.mc @@ -0,0 +1,215 @@ +// 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. +// +// Two versions of this module exist; each build keeps exactly one: +// * (:no_stress), in every normal build: ON is false. +// * (:stress), only with stress.jungle (`make watchdog`): the app skips +// the vault and runs StressTest.Job instead of the list. +// Connect IQ stops code that runs too long in one event ("Watchdog +// Tripped"), but only in the simulator's app runs, never in unit tests. So +// this build runs every 2D encoder, the cache packer and the run finder on +// the biggest inputs the app accepts, with TWO job steps per runner step: +// a pass shows that each step uses at most half of the watchdog budget +// (120,000 bytecodes per event on the 512 KB watches, 240,000 elsewhere, +// 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. +// docker/scripts/app-diag.sh watchdog reads the DIAG stress lines. + +import Toybox.Graphics; +import Toybox.Lang; +import Toybox.System; +import Toybox.WatchUi; + +(:stress) +module StressTest { + const ON = true; + // Steps per runner step: the safety margin being tested. + const MARGIN = 2; + // The last code's text, which stays on the screen to be decoded. + const SCREEN_TEXT = "OTPW stress"; + + // One item: an encoder for the largest input of its kind, through + // Codes.DisplayJob (encode, pack, runs) like CodeView, MARGIN steps at a time. + class Item extends Jobs.Job { + var name as String; + var steps as Number = 0; + var job as Codes.DisplayJob; + var symbology as Number; + var painter as RunsPainter? = null; + // Highest memory use seen between steps. + var peak as Number = 0; + + function initialize(label as String, type as Number, encoder as Codes.MatrixJob) { + Job.initialize(); + name = label; + symbology = type; + job = new Codes.DisplayJob(encoder, null); + } + + // The display job, then (when there are runs) the painter. + function step(budget as Number) as Boolean { + for (var i = 0; i < MARGIN; i++) { + steps++; + var used = System.getSystemStats().usedMemory; + peak = used > peak ? used : peak; + var p = painter; + if (p != null) { + if (p.step(budget)) { + return true; + } + } else if (job.step(budget)) { + var runs = job.runs; + if (runs == null) { + return true; + } + painter = new RunsPainter(runs, Codes.minModulePx(symbology)); + } + } + return false; + } + } + + // The screen: progress while encoding, the runs once an item is done. + class StressView extends WatchUi.View { + var title as String = "Stress test"; + var painter as RunsPainter? = null; + private var _first as Boolean = true; + + function initialize() { + View.initialize(); + } + + function onUpdate(dc as Graphics.Dc) as Void { + dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_WHITE); + dc.clear(); + var p = painter; + if (p != null) { + var drawn = p.draw(dc); + Diag.data("stress", "drawn name=" + title + " runs=" + p.runs.runs.size() + " direct=" + p.direct() + + " drawn=" + drawn); + painter = null; + } else { + dc.drawText(dc.getWidth() / 2, dc.getHeight() / 2, Graphics.FONT_XTINY, title, Graphics.TEXT_JUSTIFY_CENTER); + } + if (_first) { + _first = false; + Diag.firstFrame(); + start(); + } + } + } + + var view as StressView? = null; + var items as Array<Item> = [] as Array<Item>; + var current as Item? = null; + var runner as Jobs.JobRunner? = null; + + function initialView() as [WatchUi.Views] or [WatchUi.Views, WatchUi.InputDelegates] { + var v = new StressView(); + view = v; + return [v]; + } + + // The largest inputs: QR version 40 (bytes) and a long digit string at + // level H, Data Matrix 144 x 144 (ASCII) and Base256, Aztec 32 layers + // (text and binary), PDF417 as big as this screen takes; then the + // largest Data Matrix and QR that fit the screen, to be drawn (on most + // watches the QR is drawn from a bitmap, RunsPainter's other path). + function start() as Void { + var letters = repeat("abcdefghij", 290); + var digits = repeat("0123456789", 80); + var binary = repeat("é€", 120); + var screen = System.getDeviceSettings(); + var w = screen.screenWidth; + var h = screen.screenHeight; + var round = screen.screenShape == System.SCREEN_SHAPE_ROUND; + items = [ + new Item("qr_v40", Codes.QR, new Qr.QrJob(letters, 0, 0, -1)), + new Item("qr_digits_h", Codes.QR, new Qr.QrJob(digits, 3, 0, -1)), + new Item("dm_144", Codes.DATAMATRIX, new DataMatrix.DataMatrixJob(letters.substring(0, 1500) as String)), + new Item("dm_base256", Codes.DATAMATRIX, new DataMatrix.DataMatrixJob(binary)), + new Item("aztec_text", Codes.AZTEC, new Aztec.AztecJob(letters.substring(0, 1400) as String, 0)), + new Item("aztec_binary", Codes.AZTEC, new Aztec.AztecJob(binary.substring(0, 160) as String, 3)), + new Item("pdf417_text", Codes.PDF417, new Pdf417.Pdf417Job(letters.substring(0, w > 300 ? 900 : 250) as String, + 1, w, h, round)), + 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)), + // 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>; + Diag.data("stress", "start used_kb=" + System.getSystemStats().usedMemory / 1024); + next(); + } + + function next() as Void { + if (items.size() == 0) { + Diag.data("stress", "all done"); + return; + } + var item = items[0]; + items = items.slice(1, null) as Array<Item>; + current = item; + (view as StressView).title = item.name; + WatchUi.requestUpdate(); + var r = new Jobs.JobRunner(item, 1, new Lang.Method(StressTest, :onItemDone)); + runner = r; + r.start(); + } + + function onItemDone() as Void { + var item = current as Item; + var runs = item.job.runs; + Diag.data("stress", "done name=" + item.name + " steps=" + item.steps + " peak_kb=" + item.peak / 1024 + + " total_kb=" + System.getSystemStats().totalMemory / 1024 + " error=" + item.job.error + + " size=" + (runs != null ? runs.width + "x" + runs.height : "-")); + (view as StressView).painter = item.painter; + WatchUi.requestUpdate(); + // The next item starts in its own event, after the draw. + 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++) { + out += text; + } + return out; + } + + // The largest QR version that fits this screen at 2 px per module. + function largestQrVersion(w as Number, h as Number, round as Boolean) as Number { + for (var v = 40; v > 1; v--) { + var total = 17 + 4 * v + 8; + if (Codes.fits(total, total, 2, w, h, round)) { + return v; + } + } + return 1; + } + + // An ASCII payload length that fills the largest Data Matrix fitting + // this screen at 2 px per module. + function largestDataMatrix(w as Number, h as Number, round as Boolean) as Number { + var best = 1; + for (var i = 0; i < DataMatrix.SIZES.size(); i++) { + var s = DataMatrix.SIZES[i] as Array<Number>; + if (Codes.fits(s[0] + 2, s[0] + 2, 2, w, h, round)) { + best = s[3]; + } + } + return best; + } +} + +(:no_stress) +module StressTest { + const ON = false; + + function initialView() as [WatchUi.Views] or [WatchUi.Views, WatchUi.InputDelegates] { + return [new WatchUi.View()]; + } +} |
