aboutsummaryrefslogtreecommitdiff
path: root/source/util/Jobs.mc
blob: 009e16cbf0f440cb9b950ccec43bc5b8f533736a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
// Author:  cirakg & claude ai
// Summary: Resumable jobs: long work split into small steps so no single event trips the watchdog.
//
// Connect IQ kills code that runs too long without returning to the system
// ("Watchdog Tripped"). The device files allow 120,000 bytecodes per event on
// the Forerunner 255/255S and 240,000 on the other targets; the simulator does
// not enforce this in test runs, so the only protection is design: anything
// that can take long (key derivation, SHA-512 batches, 2D barcode encoding,
// large imports) is a Job whose step() does a bounded amount of work.
//
// Steps are sized for the Forerunner 255/255S (the only 512 KB targets).
// Every 768 KB target allows twice as much per event
// (tools/gen_products.py --check fails if a product breaks that rule), so
// JobRunner runs two steps per tick there, which halves the time of an
// import or a large QR code.
//
// JobRunner drives a job from a timer and calls back when it is done.
// Tests call Jobs.runToCompletion() instead, synchronously.

import Toybox.Lang;
import Toybox.System;
import Toybox.Timer;

module Jobs {

    // Base class for resumable work.
    class Job {

        function initialize() {
        }

        // Do at most `budget` units of work. Returns true when the job is done.
        function step(budget as Number) as Boolean {
            return true;
        }
    }

    // Run a job to the end without yielding. Only for tests and work that is
    // known to be small.
    function runToCompletion(job as Job, budget as Number) as Void {
        while (!job.step(budget)) {
        }
    }

    // Drives a job from a timer: one step of `budget` units every TICK_MS.
    class JobRunner {

        // Pause between steps, so the UI stays responsive.
        static const TICK_MS = 50;

        private var _job as Job;
        private var _budget as Number;
        private var _onDone as Method() as Void;
        private var _timer as Timer.Timer?;

        function initialize(job as Job, budget as Number, onDone as Method() as Void) {
            _job = job;
            _budget = budget;
            _onDone = onDone;
        }

        // Start stepping; the first step runs on the first tick.
        function start() as Void {
            var timer = new Timer.Timer();
            timer.start(method(:onTick), TICK_MS, true);
            _timer = timer;
        }

        // Stop without finishing (for example, when the user leaves the screen).
        function cancel() as Void {
            var timer = _timer;
            if (timer != null) {
                timer.stop();
                _timer = null;
            }
        }

        function onTick() as Void {
            for (var i = 0; i < stepsPerTick(); i++) {
                if (_job.step(_budget)) {
                    cancel();
                    _onDone.invoke();
                    return;
                }
            }
        }
    }

    // Wipes buffers in pieces of at most `max` bytes per step(): zeroing is
    // a byte loop in the VM (12 to 15 counted bytecodes per byte, measured
    // in `make watchdog`), so a few kilobytes take a step of their own.
    class Wiper {

        // Bytes per step: about 30,000 bytecodes, a quarter of the 255S's
        // 120,000 per event (measured: 16 KB in one event pass on the
        // 240,000-bytecode Forerunner 70).
        static const STEP_BYTES = 2048;
        // Up to this many bytes are wiped at once, without a step of their own.
        static const INLINE_BYTES = 512;

        private var _buffers as Array<ByteArray> = [] as Array<ByteArray>;
        private var _at as Number = 0;
        private var _left as Number = 0;

        function initialize() {
        }

        // Queue a buffer (null is ignored).
        function add(data as ByteArray?) as Void {
            if (data != null && data.size() > 0) {
                _buffers.add(data);
                _left += data.size();
            }
        }

        function addAll(buffers as Array<ByteArray>) as Void {
            for (var i = 0; i < buffers.size(); i++) {
                add(buffers[i]);
            }
        }

        // Bytes still to wipe.
        function left() as Number {
            return _left - _at;
        }

        // Wipe at most `max` bytes; true when everything is wiped.
        function step(max as Number) as Boolean {
            var done = 0;
            while (_buffers.size() > 0 && done < max) {
                var b = _buffers[0];
                var end = _at + (max - done);
                if (end > b.size()) {
                    end = b.size();
                }
                for (var i = _at; i < end; i++) {
                    b[i] = 0;
                }
                done += end - _at;
                if (end == b.size()) {
                    _left -= b.size();
                    _at = 0;
                    _buffers = _buffers.slice(1, null) as Array<ByteArray>;
                } else {
                    _at = end;
                }
            }
            return _buffers.size() == 0;
        }
    }

    // App memory above this means a 240,000-bytecode watchdog.
    const LARGE_WATCH_MEMORY = 600000;
    var _stepsPerTick as Number = 0;

    // 1 on the 512 KB watches (120,000 bytecodes per event), 2 elsewhere.
    function stepsPerTick() as Number {
        if (_stepsPerTick == 0) {
            _stepsPerTick = System.getSystemStats().totalMemory > LARGE_WATCH_MEMORY ? 2 : 1;
        }
        return _stepsPerTick;
    }
}