on master help sign in
Merge branch 'debugger'
commit7bc469081ac87d98f392e92f622639e2431d1085 authorEvan Czaplicki <info@elm-lang.org> dateFri 15 Aug 2014, 13:01 (12 years ago) parent489e5cc924ed91
9 files changed+750−23
libraries/Debug.elm+21−1 libraries/Native/Debug.js+40−4 libraries/Native/Show.js+1−1 libraries/Native/Signal/Signal.js+24−2 libraries/Native/Signal/Time.js+15−9 libraries/Native/Signal/Touch.js+2−2 libraries/Native/Signal/Window.js+2−1 runtime/Debugger.js+604 runtime/Init.js+41−3
libraries/Debug.elm+21−1
@@ -2,9 +2,14 @@ module Debug where
22{-| This library is for investigating bugs or performance problems. It should
33*not* be used in production code.
44
5+# Console Debugging
56@docs log, crash
7+
8+# Time-Travel Debugging
9+@docs watch, watchSummary, trace
610-}
711
12+import Graphics.Collage (Form)
813import Native.Debug
914import Native.Error
1015
@@ -43,4 +48,19 @@ writing a function.
4348Use the `Maybe` or `Either` libraries instead.
4449-}
4550crash : String -> a
46−crash = Native.Error.raise
51+crash = Native.Error.raise
52+
53+{-| Watch a particular value in the reactive debugger.
54+-}
55+watch : String -> a -> a
56+watch = Native.Debug.watch
57+
58+{-| Watch a particular value in the reactive debugger.
59+-}
60+watchSummary : String -> (a -> b) -> a -> a
61+watchSummary = Native.Debug.watchSummary
62+
63+{-| Trace a Form over time in the reactive debugger.
64+-}
65+trace : String -> Form -> Form
66+trace = Native.Debug.tracePath
libraries/Native/Debug.js+40−4
@@ -3,10 +3,13 @@ Elm.Native.Debug.make = function(elm) {
33 elm.Native = elm.Native || {};
44 elm.Native.Debug = elm.Native.Debug || {};
55 if (elm.Native.Debug.values) return elm.Native.Debug.values;
6+ if ('values' in Elm.Native.Debug)
7+ return elm.Native.Debug.values = Elm.Native.Debug.values;
68
79 var show = Elm.Native.Show.make(elm).show;
10+ var replace = Elm.Native.Utils.make(elm).replace;
811
9− function log(tag,value) {
12+ function log(tag, value) {
1013 var msg = tag + ': ' + show(value);
1114 var process = process || {};
1215 if (process.stdout) {
@@ -17,8 +20,41 @@ Elm.Native.Debug.make = function(elm) {
1720 return value;
1821 }
1922
20− return elm.Native.Debug.values = {
21− log: F2(log)
23+ function tracePath(debugId, form) {
24+ return replace([["debugTracePathId",debugId]], form);
25+ }
26+
27+ function WatchTracker() {
28+ this.frames = [{}];
29+ this.clear = function() {
30+ this.watches = {};
31+ };
32+ this.pushFrame = function() {
33+ var lastFrame = this.frames[this.frames.length - 1];
34+ this.frames.push(lastFrame);
35+ }
36+ this.notify = function(tag, value) {
37+ this.frames[this.frames.length - 1][tag] = value;
38+ };
39+ }
40+ var watchTracker = new WatchTracker();
41+
42+ function watch(tag, value) {
43+ watchTracker.notify(tag, value);
44+ return value;
45+ }
46+
47+ function watchSummary(tag, f, value) {
48+ watchTracker.notify(tag, f(value));
49+ return value;
50+ }
51+
52+ Elm.Native.Debug.values = {
53+ tracePath: F2(tracePath),
54+ log: F2(log),
55+ watch: F2(watch),
56+ watchSummary:F3(watchSummary),
57+ watchTracker: watchTracker
2258 };
23−
59+ return elm.Native.Debug.values = Elm.Native.Debug.values;
2460};
libraries/Native/Show.js+1−1
@@ -18,7 +18,7 @@ Elm.Native.Show.make = function(elm) {
1818 } else if (type === "boolean") {
1919 return v ? "True" : "False";
2020 } else if (type === "number") {
21− return v+"";
21+ return v.toFixed(2).replace(/\.0+$/g, '');
2222 } else if ((v instanceof String) && v.isChar) {
2323 return "'" + addSlashes(v) + "'";
2424 } else if (type === "string") {
libraries/Native/Signal/Signal.js+24−2
@@ -17,6 +17,8 @@ Elm.Native.Signal.make = function(elm) {
1717 }
1818
1919 function Input(base) {
20+ this.nodeType = "input";
21+
2022 this.id = Utils.guid();
2123 this.value = base;
2224 this.kids = [];
@@ -31,6 +33,8 @@ Elm.Native.Signal.make = function(elm) {
3133 }
3234
3335 function LiftN(update, args) {
36+ this.nodeType = "lift";
37+
3438 this.id = Utils.guid();
3539 this.value = update();
3640 this.kids = [];
@@ -86,6 +90,8 @@ Elm.Native.Signal.make = function(elm) {
8690 }
8791
8892 function Foldp(step, state, input) {
93+ this.nodeType = "foldp";
94+
8995 this.id = Utils.guid();
9096 this.value = state;
9197 this.kids = [];
@@ -104,6 +110,8 @@ Elm.Native.Signal.make = function(elm) {
104110 }
105111
106112 function DropIf(pred,base,input) {
113+ this.nodeType = "dropIf";
114+
107115 this.id = Utils.guid();
108116 this.value = pred(input.value) ? base : input.value;
109117 this.kids = [];
@@ -116,6 +124,8 @@ Elm.Native.Signal.make = function(elm) {
116124 }
117125
118126 function DropRepeats(input) {
127+ this.nodeType = "dropRepeats";
128+
119129 this.id = Utils.guid();
120130 this.value = input.value;
121131 this.kids = [];
@@ -128,11 +138,14 @@ Elm.Native.Signal.make = function(elm) {
128138 }
129139
130140 function timestamp(a) {
131− function update() { return Utils.Tuple2(Date.now(), a.value); }
141+ function update() { return Utils.Tuple2(elm.timer.now(), a.value); }
132142 return new LiftN(update, [a]);
133143 }
134144
135145 function SampleOn(s1,s2) {
146+ this.nodeType = "sampleOn";
147+ this.indirectParent = s1;
148+
136149 this.id = Utils.guid();
137150 this.value = s2.value;
138151 this.kids = [];
@@ -157,6 +170,8 @@ Elm.Native.Signal.make = function(elm) {
157170 function sampleOn(s1,s2) { return new SampleOn(s1,s2); }
158171
159172 function delay(t,s) {
173+ this.nodeType = "delay";
174+
160175 var delayed = new Input(s.value);
161176 var firstEvent = true;
162177 function update(v) {
@@ -168,6 +183,8 @@ Elm.Native.Signal.make = function(elm) {
168183 }
169184
170185 function Merge(s1,s2) {
186+ this.nodeType = "merge";
187+
171188 this.id = Utils.guid();
172189 this.value = s1.value;
173190 this.kids = [];
@@ -199,7 +216,12 @@ Elm.Native.Signal.make = function(elm) {
199216 function merges(ss) { return A2(foldr1, F2(merge), ss); }
200217
201218 return elm.Native.Signal.values = {
202− constant : function(v) { return new Input(v); },
219+ input: function(v) { return new Input(v); },
220+ constant : function(v) {
221+ var s = new Input(v);
222+ s.nodeType = "constant";
223+ return s;
224+ },
203225 lift : F2(lift ),
204226 lift2 : F3(lift2),
205227 lift3 : F4(lift3),
libraries/Native/Signal/Time.js+15−9
@@ -12,11 +12,15 @@ Elm.Native.Time.make = function(elm) {
1212
1313 function fpsWhen(desiredFPS, isOn) {
1414 var msPerFrame = 1000 / desiredFPS;
15− var prev = Date.now(), curr = prev, diff = 0, wasOn = true;
16− var ticker = Signal.constant(diff);
17− function tick(zero) { return function() {
18− curr = Date.now();
15+ var prev = elm.timer.now(), curr = prev, diff = 0, wasOn = true;
16+ var ticker = NS.input(diff);
17+ function tick(zero) {
18+ return function() {
19+ curr = elm.timer.now();
1920 diff = zero ? 0 : curr - prev;
21+ if (prev > curr) {
22+ diff = 0;
23+ }
2024 prev = curr;
2125 elm.notify(ticker.id, diff);
2226 };
@@ -24,7 +28,7 @@ Elm.Native.Time.make = function(elm) {
2428 var timeoutID = 0;
2529 function f(isOn, t) {
2630 if (isOn) {
27− timeoutID = setTimeout(tick(!wasOn && isOn), msPerFrame);
31+ timeoutID = elm.setTimeout(tick(!wasOn && isOn), msPerFrame);
2832 } else if (wasOn) {
2933 clearTimeout(timeoutID);
3034 }
@@ -35,10 +39,12 @@ Elm.Native.Time.make = function(elm) {
3539 }
3640
3741 function every(t) {
38− var clock = Signal.constant(Date.now());
39− setInterval(function() {
40− elm.notify(clock.id, Date.now());
41− }, t);
42+ var clock = NS.input(elm.timer.now());
43+ function tellTime() {
44+ elm.notify(clock.id, elm.timer.now());
45+ elm.setTimeout(tellTime, t);
46+ }
47+ elm.setTimeout(tellTime, t);
4248 return clock;
4349 }
4450
libraries/Native/Signal/Touch.js+2−2
@@ -61,11 +61,11 @@ Elm.Native.Touch.make = function(elm) {
6161 dict.insert(e.identifier,
6262 {x: point._0,
6363 y: point._1,
64− t: Date.now()});
64+ t: elm.timer.now()});
6565 }
6666 function end(e) {
6767 var t = dict.remove(e.identifier);
68− if (Date.now() - t.t < tapTime) {
68+ if (elm.timer.now() - t.t < tapTime) {
6969 hasTap = true;
7070 tap = {_:{}, x:t.x, y:t.y};
7171 }
libraries/Native/Signal/Window.js+2−1
@@ -6,6 +6,7 @@ Elm.Native.Window.make = function(elm) {
66 if (elm.Native.Window.values) return elm.Native.Window.values;
77
88 var Signal = Elm.Signal.make(elm);
9+ var NS = Elm.Native.Signal.make(elm);
910 var Tuple2 = Elm.Native.Utils.make(elm).Tuple2;
1011
1112 function getWidth() { return elm.node.clientWidth; }
@@ -16,7 +17,7 @@ Elm.Native.Window.make = function(elm) {
1617 return elm.node.clientHeight;
1718 }
1819
19− var dimensions = Signal.constant(Tuple2(getWidth(), getHeight()));
20+ var dimensions = NS.input(Tuple2(getWidth(), getHeight()));
2021 dimensions.defaultNumberOfKids = 2;
2122
2223 // Do not move width and height into Elm. By setting the default number of kids,
runtime/Debugger.jsnew file+604
@@ -0,0 +1,604 @@
1+(function() {
2+'use strict';
3+
4+if (!window.location.origin) {
5+ window.location.origin =
6+ window.location.protocol + "//" +
7+ window.location.hostname +
8+ (window.location.port ? (':' + window.location.port) : '');
9+}
10+
11+Elm.fullscreenDebugHooks = function(module, hotSwapState /* =undefined */) {
12+ var exposedDebugger = {};
13+ function debuggerAttach(module, hotSwapState) {
14+ return {
15+ make: function(runtime) {
16+ var wrappedModule = debugModule(module, runtime);
17+ exposedDebugger = debuggerInit(wrappedModule, runtime, hotSwapState);
18+ return wrappedModule.debuggedModule;
19+ }
20+ }
21+ }
22+ var mainHandle = Elm.fullscreen(debuggerAttach(module,hotSwapState));
23+ mainHandle.debugger = exposedDebugger;
24+ return mainHandle;
25+};
26+
27+var EVENTS_PER_SAVE = 100;
28+
29+function debugModule(module, runtime) {
30+ var programPaused = false;
31+ var recordedEvents = [];
32+ var asyncCallbacks = [];
33+ var snapshots = [];
34+ var watchTracker = Elm.Native.Debug.make(runtime).watchTracker;
35+ var pauseTime = 0;
36+ var eventsUntilSnapshot = EVENTS_PER_SAVE;
37+ runtime.debuggerStatus = runtime.debuggerStatus || {};
38+ runtime.debuggerStatus.eventCounter = runtime.debuggerStatus.eventCounter || 0;
39+
40+ // runtime is the prototype of wrappedRuntime
41+ // so we can access all runtime properties too
42+ var wrappedRuntime = Object.create(runtime);
43+ wrappedRuntime.notify = notifyWrapper;
44+ wrappedRuntime.setTimeout = setTimeoutWrapper;
45+
46+ // make a copy of the wrappedRuntime
47+ var assignedPropTracker = Object.create(wrappedRuntime);
48+ var debuggedModule = module.make(assignedPropTracker);
49+
50+ // make sure the signal graph is actually a signal & extract the visual model
51+ if ( !('recv' in debuggedModule.main) ) {
52+ debuggedModule.main = Elm.Signal.make(runtime).constant(debuggedModule.main);
53+ }
54+
55+ // The main module stores imported modules onto the runtime.
56+ // To ensure only one instance of each module is created,
57+ // we assign them back on the original runtime object.
58+ Object.keys(assignedPropTracker).forEach(function(key) {
59+ runtime[key] = assignedPropTracker[key];
60+ });
61+
62+ var signalGraphNodes = flattenSignalGraph(wrappedRuntime.inputs);
63+ var tracePath = tracePathInit(runtime, debuggedModule.main);
64+
65+ snapshots.push(snapshotSignalGraph(signalGraphNodes));
66+
67+ function notifyWrapper(id, v) {
68+ var timestep = runtime.timer.now();
69+
70+ if (programPaused) {
71+ // ignore async events generated while playing back
72+ // or user events while program is paused
73+ return false;
74+ }
75+ else {
76+ recordEvent(id, v, timestep);
77+ var changed = runtime.notify(id, v, timestep);
78+ snapshotOnCheckpoint();
79+ if (parent.window) {
80+ parent.window.postMessage("elmNotify", window.location.origin);
81+ }
82+ return changed;
83+ }
84+ };
85+
86+ function setTimeoutWrapper(func, delayMs) {
87+ if (programPaused) {
88+ // Don't push timers and such to the callback stack while we're paused.
89+ // It causes too many callbacks to be fired during unpausing.
90+ return 0;
91+ }
92+ var cbObj = { func:func, delayMs:delayMs, timerId:0, executed:false };
93+ var timerId = setTimeout(function() {
94+ cbObj.executed = true;
95+ func();
96+ }, delayMs);
97+ cbObj.timerId = timerId;
98+ asyncCallbacks.push(cbObj);
99+ return timerId;
100+ }
101+
102+ function recordEvent(id, v, timestep) {
103+ watchTracker.pushFrame();
104+ recordedEvents.push({ id:id, value:v, timestep:timestep });
105+ runtime.debuggerStatus.eventCounter += 1;
106+ }
107+
108+ function clearAsyncCallbacks() {
109+ asyncCallbacks.forEach(function(timer) {
110+ if (!timer.executed) {
111+ clearTimeout(timer.timerId);
112+ }
113+ });
114+ }
115+
116+ function clearRecordedEvents() {
117+ recordedEvents = [];
118+ runtime.debuggerStatus.eventCounter = 0;
119+ }
120+
121+ function getRecordedEventsLength() {
122+ return recordedEvents.length;
123+ }
124+
125+ function getRecordedEventAt(i) {
126+ return recordedEvents[i];
127+ }
128+
129+ function copyRecordedEvents() {
130+ return recordedEvents.slice();
131+ }
132+
133+ function loadRecordedEvents(events) {
134+ recordedEvents = events.slice();
135+ }
136+
137+ function clearSnapshots() {
138+ snapshots = [snapshotSignalGraph(signalGraphNodes)];
139+ }
140+
141+ function getSnapshotAt(i) {
142+ var snapshotEvent = Math.floor(i / EVENTS_PER_SAVE);
143+ assert(snapshotEvent < snapshots.length && snapshotEvent >= 0,
144+ "Out of bounds index: " + snapshotEvent);
145+ return snapshots[snapshotEvent];
146+ }
147+
148+ function snapshotOnCheckpoint() {
149+ if (eventsUntilSnapshot === 1) {
150+ snapshots.push(snapshotSignalGraph(signalGraphNodes));
151+ eventsUntilSnapshot = EVENTS_PER_SAVE;
152+ } else {
153+ eventsUntilSnapshot -= 1;
154+ }
155+ }
156+
157+ function setPaused() {
158+ programPaused = true;
159+ clearAsyncCallbacks();
160+ pauseTime = Date.now();
161+ tracePath.stopRecording();
162+ preventInputEvents();
163+ }
164+
165+ function setContinue(position) {
166+ var pauseDelay = Date.now() - pauseTime;
167+ runtime.timer.addDelay(pauseDelay);
168+ programPaused = false;
169+
170+ // we need to dump the events that are ahead of where we're continuing.
171+ var lastSnapshotPosition = Math.floor(position / EVENTS_PER_SAVE);
172+ eventsUntilSnapshot = EVENTS_PER_SAVE - (position % EVENTS_PER_SAVE);
173+ snapshots = snapshots.slice(0, lastSnapshotPosition + 1);
174+
175+ if (position < getRecordedEventsLength()) {
176+ var lastEventTime = recordedEvents[position].timestep;
177+ var scrubTime = runtime.timer.now() - lastEventTime;
178+ runtime.timer.addDelay(scrubTime);
179+ }
180+
181+ recordedEvents = recordedEvents.slice(0, position);
182+ tracePath.clearTracesAfter(position);
183+ runtime.debuggerStatus.eventCounter = position;
184+ executeCallbacks(asyncCallbacks);
185+ permitInputEvents();
186+
187+ tracePath.startRecording();
188+ }
189+
190+ function getPaused() {
191+ return programPaused;
192+ }
193+
194+ function preventInputEvents(){
195+ var events =
196+ [ "click", "mousemove", "mouseup", "mousedown", "mouseclick"
197+ , "keydown", "keypress", "keyup", "touchstart", "touchend"
198+ , "touchcancel", "touchleave", "touchmove", "pointermove"
199+ , "pointerdown", "pointerup", "pointerover", "pointerout"
200+ , "pointerenter", "pointerleave", "pointercancel"
201+ ];
202+
203+ var ignore = function(e) {
204+ var evt = e ? e : window.event;
205+ if (evt.stopPropagation) {
206+ evt.stopPropagation();
207+ }
208+ if (evt.cancelBubble !== null) {
209+ evt.cancelBubble = true;
210+ }
211+ if (evt.preventDefault) {
212+ evt.preventDefault();
213+ }
214+ return false;
215+ };
216+
217+ var ignoringDiv = document.getElementById("elmEventIgnorer");
218+ if (!ignoringDiv) {
219+ ignoringDiv = document.createElement("div");
220+ ignoringDiv.id = "elmEventIgnorer";
221+ ignoringDiv.style.position = "absolute";
222+ ignoringDiv.style.top = "0px";
223+ ignoringDiv.style.left = "0px";
224+ ignoringDiv.style.width = "100%";
225+ ignoringDiv.style.height = "100%";
226+
227+ for (var i = events.length; i-- ;) {
228+ ignoringDiv.addEventListener(events[i], ignore, true);
229+ }
230+ runtime.node.appendChild(ignoringDiv);
231+ }
232+ }
233+
234+ function permitInputEvents(){
235+ var ignoringDiv = document.getElementById("elmEventIgnorer");
236+ ignoringDiv.parentNode.removeChild(ignoringDiv);
237+ }
238+
239+ return {
240+ debuggedModule: debuggedModule,
241+ signalGraphNodes: signalGraphNodes,
242+ initialSnapshot: snapshotSignalGraph(signalGraphNodes),
243+ initialAsyncCallbacks: asyncCallbacks.slice(),
244+ // API functions
245+ clearAsyncCallbacks: clearAsyncCallbacks,
246+ clearRecordedEvents: clearRecordedEvents,
247+ getRecordedEventsLength: getRecordedEventsLength,
248+ getRecordedEventAt: getRecordedEventAt,
249+ copyRecordedEvents: copyRecordedEvents,
250+ loadRecordedEvents: loadRecordedEvents,
251+ clearSnapshots: clearSnapshots,
252+ getSnapshotAt: getSnapshotAt,
253+ snapshotOnCheckpoint: snapshotOnCheckpoint,
254+ getPaused: getPaused,
255+ setPaused: setPaused,
256+ setContinue: setContinue,
257+ tracePath: tracePath,
258+ watchTracker: watchTracker
259+ };
260+}
261+
262+// The debuggerHistory variable is passed in on hotswap. It represents
263+// the a state of the debugger for it to assume during init. It contains
264+// the paused state of the debugger, the recorded events, and the current
265+// event being processed.
266+function debuggerInit(debugModule, runtime, debuggerHistory /* =undefined */) {
267+ var currentEventIndex = 0;
268+
269+ function resetProgram(position) {
270+ var closestSnapshot = debugModule.getSnapshotAt(position);
271+ debugModule.clearAsyncCallbacks();
272+ restoreSnapshot(debugModule.signalGraphNodes, closestSnapshot);
273+ redrawGraphics();
274+ }
275+
276+ function restartProgram() {
277+ pauseProgram();
278+ resetProgram(0);
279+ debugModule.watchTracker.clear();
280+ debugModule.tracePath.clearTraces();
281+ debugModule.clearRecordedEvents();
282+ debugModule.clearSnapshots();
283+ debugModule.setContinue(0);
284+ executeCallbacks(debugModule.initialAsyncCallbacks);
285+ }
286+
287+ function pauseProgram() {
288+ debugModule.setPaused();
289+ currentEventIndex = debugModule.getRecordedEventsLength();
290+ }
291+
292+ function continueProgram() {
293+ if (debugModule.getPaused())
294+ {
295+ var closestSnapshotIndex =
296+ Math.floor(currentEventIndex / EVENTS_PER_SAVE) * EVENTS_PER_SAVE;
297+ resetProgram(currentEventIndex);
298+ var continueIndex = currentEventIndex;
299+ currentEventIndex = closestSnapshotIndex;
300+ stepTo(continueIndex);
301+ debugModule.setContinue(currentEventIndex);
302+ }
303+ }
304+
305+ function stepTo(index) {
306+ if (!debugModule.getPaused()) {
307+ debugModule.setPaused();
308+ resetProgram();
309+ }
310+
311+ if (index < 0 || index > getMaxSteps()) {
312+ throw "Index out of bounds: " + index;
313+ }
314+
315+ if (index < currentEventIndex) {
316+ var closestSnapshotIndex = Math.floor(index / EVENTS_PER_SAVE) * EVENTS_PER_SAVE;
317+ resetProgram(index);
318+ currentEventIndex = closestSnapshotIndex;
319+ }
320+
321+ while (currentEventIndex < index) {
322+ var nextEvent = debugModule.getRecordedEventAt(currentEventIndex);
323+ runtime.notify(nextEvent.id, nextEvent.value, nextEvent.timestep);
324+
325+ currentEventIndex += 1;
326+ }
327+ }
328+
329+ function getMaxSteps() {
330+ return debugModule.getRecordedEventsLength();
331+ }
332+
333+ function redrawGraphics() {
334+ var main = debugModule.debuggedModule.main
335+ for (var i = main.kids.length ; i-- ; ) {
336+ main.kids[i].recv(runtime.timer.now(), true, main.id);
337+ }
338+ }
339+
340+ function getHotSwapState() {
341+ var continueIndex = currentEventIndex;
342+ if (!debugModule.getPaused()) {
343+ continueIndex = getMaxSteps();
344+ }
345+ return {
346+ paused: debugModule.getPaused(),
347+ recordedEvents: debugModule.copyRecordedEvents(),
348+ currentEventIndex: continueIndex
349+ };
350+ }
351+
352+ function dispose() {
353+ var parentNode = runtime.node.parentNode;
354+ parentNode.removeChild(debugModule.tracePath.canvas);
355+ parentNode.removeChild(runtime.node);
356+ }
357+
358+ if (debuggerHistory) {
359+ // The problem is that we want to previous paused state. But
360+ // by the time JS reaches here, the old code has been swapped out
361+ // and the new modules are being generated. So we can ask the
362+ // debugging console what it thinks the pause state is and go
363+ // from there.
364+ var paused = debuggerHistory.paused;
365+ debugModule.setPaused();
366+ debugModule.loadRecordedEvents(debuggerHistory.recordedEvents);
367+ var index = getMaxSteps();
368+ runtime.debuggerStatus.eventCounter = 0;
369+ debugModule.tracePath.clearTraces();
370+
371+ // draw new trace path
372+ debugModule.tracePath.startRecording();
373+ while(currentEventIndex < index) {
374+ var nextEvent = debugModule.getRecordedEventAt(currentEventIndex);
375+ runtime.debuggerStatus.eventCounter += 1;
376+ runtime.notify(nextEvent.id, nextEvent.value, nextEvent.timestep);
377+ debugModule.snapshotOnCheckpoint();
378+ currentEventIndex += 1;
379+ }
380+ debugModule.tracePath.stopRecording();
381+
382+ stepTo(debuggerHistory.currentEventIndex);
383+ if (!paused) {
384+ debugModule.setContinue(debuggerHistory.currentEventIndex);
385+ }
386+ }
387+
388+ runtime.node.parentNode.appendChild(debugModule.tracePath.canvas);
389+
390+ var elmDebugger = {
391+ restart: restartProgram,
392+ pause: pauseProgram,
393+ kontinue: continueProgram,
394+ getMaxSteps: getMaxSteps,
395+ stepTo: stepTo,
396+ getPaused: debugModule.getPaused,
397+ getHotSwapState: getHotSwapState,
398+ dispose: dispose,
399+ allNodes: debugModule.signalGraphNodes,
400+ watchTracker: debugModule.watchTracker
401+ };
402+
403+ return elmDebugger;
404+}
405+
406+function Point(x, y) {
407+ this.x = x;
408+ this.y = y;
409+
410+ this.translate = function(x, y) {
411+ return new Point(this.x + x, this.y + y);
412+ }
413+
414+ this.equals = function(p) {
415+ return this.x == p.x && this.y == p.y;
416+ }
417+}
418+
419+function tracePathInit(runtime, signalGraphMain) {
420+ var List = Elm.List.make(runtime);
421+ var Signal = Elm.Signal.make(runtime);
422+ var tracePathNode = A2(Signal.lift, graphicsUpdate, signalGraphMain);
423+ var tracePathCanvas = createCanvas();
424+ var tracePositions = {};
425+ var recordingTraces = true;
426+
427+ function findPositions(currentScene) {
428+ var positions = {};
429+ function processElement(elem, offset) {
430+ if (elem.element.ctor == "Custom" && elem.element.type == "Collage")
431+ {
432+ List.map(F2(processForm)(offset))(elem.element.model.forms);
433+ }
434+ }
435+
436+ function processForm(offset, form) {
437+ if (form.form.ctor == "FElement")
438+ {
439+ processElement(form.form._0, offset.translate(form.x, -form.y));
440+ }
441+ if (form.form.ctor == "FGroup")
442+ {
443+ var newOffset = offset.translate(form.x, -form.y);
444+ List.map(F2(processForm)(newOffset))(form.form._1);
445+ }
446+ if (form.debugTracePathId)
447+ {
448+ positions[form.debugTracePathId] = new Point(form.x + offset.x, -form.y + offset.y);
449+ }
450+ }
451+
452+ processElement(currentScene, new Point(0, 0));
453+ return positions;
454+ }
455+
456+ function appendPositions(positions) {
457+ for (var id in positions) {
458+ var pos = positions[id];
459+ if (tracePositions.hasOwnProperty(id)) {
460+ tracePositions[id].push(pos);
461+ }
462+ else {
463+ tracePositions[id] = [pos];
464+ }
465+ if (tracePositions[id].length < runtime.debuggerStatus.eventCounter) {
466+ var padCount = runtime.debuggerStatus.eventCounter - tracePositions[id].length;
467+ var lastTracePosition = tracePositions[id][tracePositions[id].length - 1];
468+ for (var i = padCount; i--;) {
469+ tracePositions[id].push(lastTracePosition)
470+ }
471+ }
472+ assert(tracePositions[id].length === runtime.debuggerStatus.eventCounter,
473+ "We don't have a 1-1 mapping of trace positions to events");
474+ }
475+ }
476+
477+ function graphicsUpdate(currentScene) {
478+ if (!recordingTraces) {
479+ return;
480+ }
481+
482+ var ctx = tracePathCanvas.getContext('2d');
483+ ctx.clearRect(0, 0, tracePathCanvas.width, tracePathCanvas.height);
484+
485+ ctx.save();
486+ ctx.translate(ctx.canvas.width/2, ctx.canvas.height/2);
487+ appendPositions(findPositions(currentScene));
488+ for (var id in tracePositions)
489+ {
490+ ctx.beginPath();
491+ var points = tracePositions[id];
492+ for (var i=0; i < points.length; i++)
493+ {
494+ var p = points[i];
495+ if (i == 0) {
496+ ctx.moveTo(p.x, p.y);
497+ }
498+ else {
499+ ctx.lineTo(p.x, p.y);
500+ }
501+ }
502+ ctx.lineWidth = 1;
503+ ctx.strokeStyle = "rgba(50, 50, 50, 0.4)";
504+ ctx.stroke();
505+ }
506+
507+ ctx.restore();
508+ }
509+
510+ function clearTraces() {
511+ tracePositions = {};
512+ }
513+
514+ function stopRecording() {
515+ recordingTraces = false;
516+ }
517+
518+ function startRecording() {
519+ recordingTraces = true;
520+ }
521+
522+ function clearTracesAfter(position) {
523+ var newTraces = {};
524+ for (var id in tracePositions) {
525+ newTraces[id] = tracePositions[id].slice(0,position);
526+ }
527+ tracePositions = newTraces;
528+ }
529+
530+ return {
531+ graphicsUpdate: graphicsUpdate,
532+ canvas: tracePathCanvas,
533+ clearTraces: clearTraces,
534+ clearTracesAfter: clearTracesAfter,
535+ stopRecording: stopRecording,
536+ startRecording: startRecording
537+ }
538+}
539+
540+function executeCallbacks(callbacks) {
541+ callbacks.forEach(function(timer) {
542+ if (!timer.executed) {
543+ var func = timer.func;
544+ timer.executed = true;
545+ func();
546+ }
547+ });
548+}
549+
550+function createCanvas() {
551+ var c = document.createElement('canvas');
552+ c.width = window.innerWidth;
553+ c.height = window.innerHeight;
554+ c.style.position = "absolute";
555+ c.style.top = "0";
556+ c.style.left = "0";
557+ c.style.pointerEvents = "none";
558+ return c;
559+}
560+
561+function assert(bool, msg) {
562+ if (!bool) {
563+ throw "Assertion error: " + msg;
564+ }
565+}
566+
567+function snapshotSignalGraph(signalGraphNodes) {
568+ var nodeValues = [];
569+
570+ signalGraphNodes.forEach(function(node) {
571+ nodeValues.push({ value: node.value, id: node.id });
572+ });
573+
574+ return nodeValues;
575+};
576+
577+function restoreSnapshot(signalGraphNodes, snapshot) {
578+ assert(signalGraphNodes.length == snapshot.length,
579+ "saved program state has wrong length");
580+ for (var i=0; i < signalGraphNodes.length; i++) {
581+ var node = signalGraphNodes[i];
582+ var state = snapshot[i];
583+ assert(node.id == state.id, "the nodes moved position");
584+
585+ node.value = state.value;
586+ }
587+}
588+
589+function flattenSignalGraph(nodes) {
590+ var nodesById = {};
591+
592+ function addAllToDict(node) {
593+ nodesById[node.id] = node;
594+ node.kids.forEach(addAllToDict);
595+ }
596+ nodes.forEach(addAllToDict);
597+
598+ var allNodes = Object.keys(nodesById).sort().map(function(key) {
599+ return nodesById[key];
600+ });
601+ return allNodes;
602+};
603+
604+}());
runtime/Init.js+41−3
@@ -31,6 +31,29 @@ function init(display, container, module, ports, moduleToReplace) {
3131 // defining state needed for an instance of the Elm RTS
3232 var inputs = [];
3333
34+ /* OFFSET
35+ * Elm's time traveling debugger lets you interrupt the smooth flow of time
36+ * by pausing and continuing program execution. To ensure the user sees a
37+ * program that moves smoothly through the pause/continue time gap,
38+ * we need to adjsut the value of Date.now().
39+ */
40+ var timer = function() {
41+ var inducedDelay = 0;
42+
43+ var now = function() {
44+ return Date.now() - inducedDelay;
45+ };
46+
47+ var addDelay = function(d) {
48+ inducedDelay += d;
49+ return inducedDelay;
50+ };
51+
52+ return { now : now
53+ , addDelay : addDelay
54+ }
55+ }();
56+
3457 var updateInProgress = false;
3558 function notify(id, v) {
3659 if (updateInProgress) {
@@ -40,12 +63,15 @@ function init(display, container, module, ports, moduleToReplace) {
4063 'Definitely report this to <https://github.com/elm-lang/Elm/issues>\n');
4164 }
4265 updateInProgress = true;
43− var timestep = Date.now();
66+ var timestep = timer.now();
4467 for (var i = inputs.length; i--; ) {
4568 inputs[i].recv(timestep, id, v);
4669 }
4770 updateInProgress = false;
4871 }
72+ function setTimeout(func, delay) {
73+ window.setTimeout(func, delay);
74+ }
4975
5076 var listeners = [];
5177 function addListener(relevantInputs, domNode, eventName, func) {
@@ -67,11 +93,13 @@ function init(display, container, module, ports, moduleToReplace) {
6793 // object. This permits many Elm programs to be embedded per document.
6894 var elm = {
6995 notify:notify,
96+ setTimeout:setTimeout,
7097 node:container,
7198 display:display,
7299 id:ElmRuntime.guid(),
73100 addListener:addListener,
74101 inputs:inputs,
102+ timer:timer,
75103 ports: { incoming:ports, outgoing:{}, uses:portUses }
76104 };
77105
@@ -84,6 +112,11 @@ function init(display, container, module, ports, moduleToReplace) {
84112 return newElm;
85113 }
86114
115+ function dispose() {
116+ removeListeners(listeners);
117+ inputs = [];
118+ }
119+
87120 var Module = {};
88121 var reportAnyErrors = function() {};
89122 try {
@@ -110,7 +143,11 @@ function init(display, container, module, ports, moduleToReplace) {
110143 }
111144
112145 reportAnyErrors();
113− return { swap:swap, ports:elm.ports.outgoing };
146+ return {
147+ swap:swap,
148+ ports:elm.ports.outgoing,
149+ dispose:dispose
150+ };
114151};
115152
116153function checkPorts(elm) {
@@ -203,8 +240,9 @@ function addReceivers(ports) {
203240}
204241
205242function initGraphics(elm, Module) {
206− if (!('main' in Module))
243+ if (!('main' in Module)) {
207244 throw new Error("'main' is missing! What do I display?!");
245+ }
208246
209247 var signalGraph = Module.main;
210248