// 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 = [] as Array; 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; 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; 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; 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()]; } }