diff options
| -rw-r--r-- | source/codes/Aztec.mc | 21 | ||||
| -rw-r--r-- | source/codes/Codes.mc | 72 | ||||
| -rw-r--r-- | source/codes/DataMatrix.mc | 24 | ||||
| -rw-r--r-- | source/codes/Pdf417.mc | 21 | ||||
| -rw-r--r-- | source/codes/Qr.mc | 30 | ||||
| -rw-r--r-- | source/codes/ReedSolomon.mc | 5 | ||||
| -rw-r--r-- | source/ui/CodeView.mc | 3 | ||||
| -rw-r--r-- | test/codes/CodeCacheTest.mc | 46 |
8 files changed, 214 insertions, 8 deletions
diff --git a/source/codes/Aztec.mc b/source/codes/Aztec.mc index 495abbf..0d4a856 100644 --- a/source/codes/Aztec.mc +++ b/source/codes/Aztec.mc @@ -44,6 +44,8 @@ module Aztec { const FIELD_ENTRIES_PER_STEP = 500; // GF(1024) / GF(4096) tables const GENERATOR_WORK_PER_STEP = 500; // generator coefficient updates const ECC_WORK_PER_STEP = 400; // data symbols x EC symbols + // Cumulative percent at the end of each phase 0-5 (see AztecJob.percent). + const PHASE_ENDS = [8, 14, 20, 64, 72, 100]; const GRID_LINES_PER_STEP = 4; // reference grid const WORDS_PER_STEP = 50; // codewords into bits const PAIRS_PER_STEP = 250; // spiral bit pairs @@ -314,8 +316,25 @@ module Aztec { _ec = EC_PERCENT[option] as Number; } + // Progress 0-100: the share of each phase (simulator timings; the + // Reed-Solomon codewords and the spiral take the most). function percent() as Number { - return _phase * 100 / 6; + var done = 0; + var total = 0; + var e = _encoder; + if (_phase == 3 && e != null) { // data symbols done + done = e.percent(); + total = 100; + } else if (_phase == 5) { // bits placed + done = _at; + total = _bits.size(); + } + return Codes.phased(PHASE_ENDS, _phase, done, total); + } + + // Packing and runs: about 86 / 3 / 11 % of a whole display job. + function stageEnds() as Array<Number> { + return [86, 89]; } function matrix() as Codes.Matrix? { diff --git a/source/codes/Codes.mc b/source/codes/Codes.mc index 247acfa..9527a75 100644 --- a/source/codes/Codes.mc +++ b/source/codes/Codes.mc @@ -7,6 +7,7 @@ // come back as a CodeError with a message for the user, never an exception. import Toybox.Lang; +import Toybox.System; module Codes { @@ -174,6 +175,12 @@ module Codes { result[6] = matrix.rowHeight; } + // Progress 0-99 through the bytes. + function percent() as Number { + var full = _matrix.modules.size() / 8; + return full > 0 ? 99 * _i / full : 0; + } + function step(budget as Number) as Boolean { var m = _matrix.modules; var n = m.size(); @@ -235,6 +242,11 @@ module Codes { result = new Runs(packed); } + // Progress 0-99 through the rows. + function percent() as Number { + return 99 * _row / _packed.height; + } + function step(budget as Number) as Boolean { var p = _packed; var w = p.width; @@ -276,31 +288,63 @@ module Codes { // A 2D code from the cache or a fresh encoding to Runs: encode (unless // cached), pack (`cache` then holds the bytes to store), find the runs. + // percent() follows the work: the encoder's share of 0-100 comes from + // its stageEnds(), then the packer's, then the run finder's; a cached + // code only runs the finder, over the whole range. class DisplayJob extends Jobs.Job { var error as String? = null; var cache as ByteArray? = null; var runs as Runs? = null; + // Milliseconds spent in each stage's steps (encode, pack, runs), for + // the debug log. + var stageMs as Array<Number> = [0, 0, 0]; private var _encoder as MatrixJob?; private var _packer as Packer? = null; private var _finder as RunFinder? = null; + private var _encodeEnd as Number = 0; + private var _packEnd as Number = 0; // Either an encoder (fresh) or the cached bytes. function initialize(encoder as MatrixJob?, packed as Packed?) { Job.initialize(); _encoder = encoder; + if (encoder != null) { + var ends = encoder.stageEnds(); + _encodeEnd = ends[0]; + _packEnd = ends[1]; + } if (packed != null) { _finder = new RunFinder(packed); } } - // Progress 0-100: encoding is most of it. + // Progress 0-99 (100 is never shown: the code replaces this screen), + // never going back: each stage starts where the one before ended. function percent() as Number { var e = _encoder; - return e != null ? e.percent() * 9 / 10 : 90; + var p = _packer; + var f = _finder; + var done = 0; + if (e != null) { + done = e.percent() * _encodeEnd / 100; + } else if (p != null) { + done = _encodeEnd + (_packEnd - _encodeEnd) * p.percent() / 100; + } else if (f != null) { + done = _packEnd + (100 - _packEnd) * f.percent() / 100; + } + return done < 99 ? done : 99; } function step(budget as Number) as Boolean { + var started = System.getTimer(); + var stage = _finder != null ? 2 : (_packer != null ? 1 : 0); + var done = advance(budget); + stageMs[stage] += System.getTimer() - started; + return done; + } + + private function advance(budget as Number) as Boolean { var finder = _finder; if (finder != null) { if (!finder.step(budget)) { @@ -344,11 +388,20 @@ module Codes { Job.initialize(); } - // Progress 0-100 for the UI. + // Progress 0-100 for the UI, following the work done so far. function percent() as Number { return 0; } + // Where the pack and the run stages begin in a display job's 0-100 + // (the encoding is [0, ends[0]], packing [ends[0], ends[1]], the + // runs the rest), weighted by their time in the simulator's + // benchmark: encoding takes by far the most, except for PDF417, + // whose wide matrices make the runs a third. + function stageEnds() as Array<Number> { + return [90, 92]; + } + function matrix() as Matrix? { return null; } @@ -379,6 +432,19 @@ module Codes { return isLinear(symbology) || symbology == PDF417 ? 1 : 2; } + // Progress 0-100 through phases: ends[i] is the percent at the end of + // phase i; `done` of `total` is how far the current phase has got (it + // may be a flat 0 for a phase that reports no progress). A finished job + // (phase past the last) is 100. Never goes back while `done` doesn't. + function phased(ends as Array<Number>, phase as Number, done as Number, total as Number) as Number { + if (phase >= ends.size()) { + return 100; + } + var from = phase > 0 ? ends[phase - 1] : 0; + var inside = total > 0 && done > 0 ? (done < total ? done : total) : 0; + return from + (ends[phase] - from) * inside / (total > 0 ? total : 1); + } + // True when a code of w x h modules (quiet zone included) at px pixels // per module fits the screen: inside the circle (less a 4 px margin) on // round screens; elsewhere the full width and the height less room for diff --git a/source/codes/DataMatrix.mc b/source/codes/DataMatrix.mc index 76a29e8..4fcc179 100644 --- a/source/codes/DataMatrix.mc +++ b/source/codes/DataMatrix.mc @@ -39,6 +39,8 @@ module DataMatrix { // with a 1,500-byte payload (144 x 144 modules). const BYTES_PER_STEP = 250; // codeword encodation const ECC_WORK_PER_STEP = 400; // data symbols x EC symbols + // Cumulative percent at the end of each phase 0-4 (see DataMatrixJob.percent). + const PHASE_ENDS = [2, 44, 45, 69, 100]; const BUILD_MODULES_PER_STEP = 300; // symbol modules var field as ReedSolomon.Field? = null; @@ -184,8 +186,28 @@ module DataMatrix { _data = Bytes.fromUtf8(text); } + // Progress 0-100: the share of each phase (simulator timings; the + // Reed-Solomon blocks, the placement and the symbol take the most). function percent() as Number { - return _phase * 20; + var done = 0; + var total = 0; + if (_phase == 1 && _size.size() > 0) { // blocks done, and the current one + var e = _encoder; + done = _block * 100 + (e != null ? e.percent() : 0); + total = _size[5] * 100; + } else if (_phase == 3 && _size.size() > 0) { // codewords placed + done = _chr - 1; + total = _size[3] + _size[4]; + } else if (_phase == 4 && _size.size() > 0) { // rows built + done = _buildRow; + total = _size[0]; + } + return Codes.phased(PHASE_ENDS, _phase, done, total); + } + + // Packing and runs: about 84 / 3 / 13 % of a whole display job. + function stageEnds() as Array<Number> { + return [84, 87]; } function matrix() as Codes.Matrix? { diff --git a/source/codes/Pdf417.mc b/source/codes/Pdf417.mc index a294aa7..c905a2d 100644 --- a/source/codes/Pdf417.mc +++ b/source/codes/Pdf417.mc @@ -35,6 +35,8 @@ module Pdf417 { const NUMERIC_LATCH = 902; const ROW_HEIGHT = 3; const QUIET = 2; + // Cumulative percent at the end of each phase 0-2 (see Pdf417Job.percent). + const PHASE_ENDS = [40, 70, 100]; const MAX_CODEWORDS = 928; const KIND_TEXT = 0; @@ -383,8 +385,25 @@ module Pdf417 { level = ecLevel; } + // Progress 0-100: the share of each phase (simulator timings), and + // inside the Reed-Solomon and row phases how far they have got. function percent() as Number { - return _phase == 1 && _words.size() > 0 ? 20 + 60 * _at / _words.size() : _phase * 30; + var done = 0; + var total = 0; + if (_phase == 1) { // data codewords done + done = _at; + total = _words.size(); + } else if (_phase == 2) { // rows built + done = _at; + total = rows; + } + return Codes.phased(PHASE_ENDS, _phase, done, total); + } + + // Packing and runs: about 62 / 7 / 31 % of a whole display job (the + // wide matrices make the runs a third). + function stageEnds() as Array<Number> { + return [62, 69]; } function matrix() as Codes.Matrix? { diff --git a/source/codes/Qr.mc b/source/codes/Qr.mc index 1fac051..5ced89e 100644 --- a/source/codes/Qr.mc +++ b/source/codes/Qr.mc @@ -49,6 +49,8 @@ module Qr { const WRITE_CHARS_PER_STEP = 300; // Phase 1: data symbols times EC symbols per step (about 35 bytecodes each). const ECC_WORK_PER_STEP = 400; + // Cumulative percent at the end of each phase 0-6 (see QrJob.percent). + const PHASE_ENDS = [8, 16, 18, 27, 39, 93, 100]; // Phase 2: codewords interleaved per step. const INTERLEAVE_PER_STEP = 600; const PREP_SEGMENT = 0; @@ -366,9 +368,33 @@ module Qr { _version = forceVersion; } - // Progress 0-100 for the UI. + // Progress 0-100 for the UI: the share of each phase of the work + // (simulator timings of a version 6, a version 40 and a level H + // code; mask scoring is about half of it), and inside the long ones + // how far they have got. function percent() as Number { - return _phase * 100 / 7; + var done = 0; + var total = 0; + if (_phase == 1 && _blocks.size() > 0) { // blocks done, and the current one + var e = _encoder; + done = _cursor * 100 + (e != null ? e.percent() : 0); + total = _blocks.size() * 100; + } else if (_phase == 3) { // column pairs, right to left + done = _size - 1 - _cursor; + total = _size - 1; + } else if (_phase == 5) { // lines of the masks scored so far + done = (_maskIndex * 2 + _maskPass) * _size + _cursor; + total = 16 * _size; + } else if (_phase == 6) { // rows masked + done = _cursor; + total = _size; + } + return Codes.phased(PHASE_ENDS, _phase, done, total); + } + + // Packing and runs: about 92 / 2 / 6 % of a whole display job. + function stageEnds() as Array<Number> { + return [92, 94]; } // The code as a generic matrix with QR's 4-module quiet zone. diff --git a/source/codes/ReedSolomon.mc b/source/codes/ReedSolomon.mc index 58339ae..5fc75aa 100644 --- a/source/codes/ReedSolomon.mc +++ b/source/codes/ReedSolomon.mc @@ -171,6 +171,11 @@ module ReedSolomon { } } + // Progress 0-100 through the data symbols. + function percent() as Number { + return _data.size() > 0 ? 100 * _i / _data.size() : 100; + } + // Up to `symbols` more data symbols; true when all are in. function step(symbols as Number) as Boolean { var rem = remainder; diff --git a/source/ui/CodeView.mc b/source/ui/CodeView.mc index 20b9bf2..efa23d2 100644 --- a/source/ui/CodeView.mc +++ b/source/ui/CodeView.mc @@ -179,6 +179,9 @@ class CodeView extends WatchUi.View { _code = new Codes.CodeError(err != null ? err : "could not encode"); } else { var cache = job.cache; + var ms = job.stageMs; + Diag.data("matrix_stages", "type=" + header.type + " cached=" + (cache == null) + " encode_ms=" + ms[0] + + " pack_ms=" + ms[1] + " runs_ms=" + ms[2]); if (cache != null) { Diag.data("matrix_encoded", "type=" + header.type + " size=" + runs.width + "x" + runs.height + " ms=" + (System.getTimer() - _startedAt)); diff --git a/test/codes/CodeCacheTest.mc b/test/codes/CodeCacheTest.mc index 2ad7f05..3b2a02e 100644 --- a/test/codes/CodeCacheTest.mc +++ b/test/codes/CodeCacheTest.mc @@ -110,3 +110,49 @@ function codeRunsCoverExactlyTheDarkModules(logger as Test.Logger) as Boolean { } return true; } + +// Steps a display job to the end and checks its percentage follows the +// work: never down, never above 99, no big jumps, and a real spread (the +// "Encoding" screen once showed 90 % at the first frame and sat there). +function progressIsSteady(job as Codes.DisplayJob) as Boolean { + var last = job.percent(); + var biggest = last; + var steps = 0; + var steady = last < 35; + while (!job.step(1)) { + var now = job.percent(); + steady = steady && now >= last && now <= 99; + biggest = now - last > biggest ? now - last : biggest; + last = now; + steps++; + } + return steady && biggest <= 35 && steps >= 3 && last >= 60 && job.error == null; +} + +(:test) +function displayProgressFollowsTheWork(logger as Test.Logger) as Boolean { + var member = "https://example.com/member/fake-0042?name=Alice%20Example&card=0000111122223333&valid=2027-12-31&level=gold"; + var long = ""; + for (var i = 0; i < 40; i++) { + long += "abcdefghij"; + } + var qr = progressIsSteady(new Codes.DisplayJob(new Qr.QrJob(member, 1, 0, -1), null)); + var qrBig = progressIsSteady(new Codes.DisplayJob(new Qr.QrJob(long, 0, 0, -1), null)); + var qrForced = progressIsSteady(new Codes.DisplayJob(new Qr.QrJob(member, 1, 0, 3), null)); + var dm = progressIsSteady(new Codes.DisplayJob(new DataMatrix.DataMatrixJob(long.substring(0, 200) as String), null)); + var az = progressIsSteady(new Codes.DisplayJob(new Aztec.AztecJob(long.substring(0, 200) as String, 1), null)); + var pdf = progressIsSteady(new Codes.DisplayJob(new Pdf417.Pdf417Job(long.substring(0, 200) as String, 1, 260, 260, true), + null)); + // A cached code only runs the finder: 0 at the start, then up to 99. + var bits = new [60 * 60]b; + for (var i = 0; i < bits.size(); i++) { + bits[i] = (i * 7 / 3) % 2; + } + var packed = Codes.Packed.parse(new Codes.Matrix(60, 60, 4, bits).toBytes()) as Codes.Packed; + var cached = new Codes.DisplayJob(null, packed); + var start = cached.percent(); + var cachedSteady = progressIsSteady(cached); + logger.debug("progress qr=" + qr + " big=" + qrBig + " forced=" + qrForced + " dm=" + dm + " az=" + az + " pdf=" + pdf + + " cached=" + cachedSteady); + return qr && qrBig && qrForced && dm && az && pdf && start == 0 && cachedSteady; +} |
