on master help sign in
Merge remote-tracking branch 'laszlopandy/elm-debugger' into debugger

Conflicts: libraries/Debug.elm libraries/Native/Debug.js libraries/Native/Signal/Signal.js libraries/Native/Signal/Time.js runtime/Init.js

commitc689fef65d5713dd6fc542cbce73e3e8dafa0dd9 authorEvan Czaplicki <info@elm-lang.org> dateFri 28 Mar 2014, 12:44 (13 years ago) parent5cdb8f344d2c72
9 files changed+527−18
libraries/Debug.elm+9−2 libraries/Native/Debug.js+11−3 libraries/Native/Show.js+1−1 libraries/Native/Signal/Keyboard.js+6−3 libraries/Native/Signal/Signal.js+23−1 libraries/Native/Signal/Time.js+8−6 libraries/Native/Signal/Window.js+2−1 runtime/Debugger.js+457 runtime/Init.js+10−1
libraries/Debug.elm+9−2
@@ -2,9 +2,10 @@ module Debug where
22{-| This library is for investigating bugs or performance problems. It should
33*not* be used in production code.
44
5−@docs log
5+@docs log, trace
66-}
77
8+import Graphics.Element (Element)
89import Native.Debug
910
1011{-| Log a tagged value on the developer console, and then return the value.
@@ -16,4 +17,10 @@ Notice that `log` is not a pure function! It should *only* be used for
1617investigating bugs or performance problems.
1718-}
1819log : String -> a -> a
19−log = Native.Debug.log
20+log = Native.Debug.log
21+
22+{-| Trace an Element over time in the reactive debugger.
23+-}
24+trace : String -> Element -> Element
25+trace = Native.Debug.tracePath
26+
libraries/Native/Debug.js+11−3
@@ -3,6 +3,8 @@ 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;
810
@@ -17,8 +19,14 @@ Elm.Native.Debug.make = function(elm) {
1719 return value;
1820 }
1921
20− return elm.Native.Debug.values = {
21− log: F2(log)
22+ function tracePath(debugId, elem) {
23+ elem.props.debugTracePathId = debugId;
24+ return elem;
25+ }
26+
27+ Elm.Native.Debug.values = {
28+ tracePath: F2(tracePath),
29+ log: F2(log),
2230 };
23−
31+ return elm.Native.Debug.values = Elm.Native.Debug.values;
2432};
libraries/Native/Show.js+1−1
@@ -17,7 +17,7 @@ Elm.Native.Show.make = function(elm) {
1717 } else if (type === "boolean") {
1818 return v ? "True" : "False";
1919 } else if (type === "number") {
20− return v+"";
20+ return v.toFixed(2).replace(/\.0+$/g, '');
2121 } else if ((v instanceof String) && v.isChar) {
2222 return "'" + addSlashes(v) + "'";
2323 } else if (type === "string") {
libraries/Native/Signal/Keyboard.js+6−3
@@ -17,9 +17,10 @@ Elm.Native.Keyboard.make = function(elm) {
1717 var NList = Elm.Native.List.make(elm);
1818 var Utils = Elm.Native.Utils.make(elm);
1919
20− var downEvents = Signal.constant(0);
21− var upEvents = Signal.constant(0);
22− var blurEvents = Signal.constant(0);
20+ var NS = Elm.Native.Signal.make(elm);
21+ var downEvents = NS.input(0);
22+ var upEvents = NS.input(0);
23+ var blurEvents = NS.input(0);
2324
2425 elm.addListener([downEvents.id], document, 'keydown', function down(e) {
2526 elm.notify(downEvents.id, e.keyCode);
@@ -34,6 +35,8 @@ Elm.Native.Keyboard.make = function(elm) {
3435 });
3536
3637 function KeyMerge(down, up, blur) {
38+ this.nodeType = "merge";
39+
3740 var args = [down,up,blur];
3841 this.id = Utils.guid();
3942 // Ignore starting values here
libraries/Native/Signal/Signal.js+23−1
@@ -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 = [];
@@ -133,6 +143,9 @@ Elm.Native.Signal.make = function(elm) {
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+8−6
@@ -13,7 +13,7 @@ Elm.Native.Time.make = function(elm) {
1313 function fpsWhen(desiredFPS, isOn) {
1414 var msPerFrame = 1000 / desiredFPS;
1515 var prev = Date.now(), curr = prev, diff = 0, wasOn = true;
16− var ticker = Signal.constant(diff);
16+ var ticker = NS.input(diff);
1717 function tick(zero) { return function() {
1818 curr = Date.now();
1919 diff = zero ? 0 : curr - prev;
@@ -24,7 +24,7 @@ Elm.Native.Time.make = function(elm) {
2424 var timeoutID = 0;
2525 function f(isOn, t) {
2626 if (isOn) {
27− timeoutID = setTimeout(tick(!wasOn && isOn), msPerFrame);
27+ timeoutID = elm.runDelayed(tick(!wasOn && isOn), msPerFrame);
2828 } else if (wasOn) {
2929 clearTimeout(timeoutID);
3030 }
@@ -35,10 +35,12 @@ Elm.Native.Time.make = function(elm) {
3535 }
3636
3737 function every(t) {
38− var clock = Signal.constant(Date.now());
39− setInterval(function() {
40− elm.notify(clock.id, Date.now());
41− }, t);
38+ var clock = NS.input(Date.now());
39+ var id = elm.runDelayed(function tellTime() {
40+ if (elm.notify(clock.id, Date.now())) {
41+ elm.runDelayed(tellTime, t);
42+ }
43+ }, t);
4244 return clock;
4345 }
4446
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+457
@@ -0,0 +1,457 @@
1+
2+(function() {
3+'use strict';
4+
5+Elm.Debugger = null;
6+Elm.debuggerAttach = function(module, hotSwapState /* =undefined */) {
7+ return {
8+ make: function(runtime) {
9+ var wrappedModule = debugModule(module, runtime);
10+ Elm.Debugger = debuggerInit(wrappedModule, runtime, hotSwapState);
11+ dispatchElmDebuggerInit();
12+ return wrappedModule.moduleInstance;
13+ }
14+ };
15+};
16+
17+function dispatchElmDebuggerInit() {
18+ if (parent.window) {
19+ var dispatch = function(_) {
20+ parent.window.postMessage("elmDebuggerInit", window.location.origin);
21+ };
22+ if (parent.window.document.readyState === "complete") {
23+ dispatch();
24+ }
25+ else {
26+ parent.window.addEventListener("load", dispatch);
27+ }
28+ }
29+}
30+
31+function debugModule(module, runtime) {
32+ var programPaused = false;
33+ var recordedEvents = [];
34+ var asyncCallbacks = [];
35+
36+ function wrapNotify(id, v) {
37+ var timestep = Date.now();
38+
39+ if (programPaused) {
40+ // ignore async events generated while playing back
41+ // or user events while program is paused
42+ return false;
43+ }
44+ else {
45+ var changed = runtime.notify(id, v, timestep);
46+ recordEvent(id, v, timestep);
47+ if (parent.window) {
48+ parent.window.postMessage("elmNotify", window.location.origin);
49+ }
50+ return changed;
51+ }
52+ };
53+
54+ function wrapRunDelayed(func, delayMs) {
55+ var cbObj = { func:func, delayMs:delayMs, timerId:0, executed:false };
56+ var timerId = setTimeout(function() {
57+ cbObj.executed = true;
58+ func();
59+ }, delayMs);
60+ cbObj.timerId = timerId;
61+ asyncCallbacks.push(cbObj);
62+ return timerId;
63+ }
64+
65+ function recordEvent(id, v, timestep) {
66+ recordedEvents.push({ id:id, value:v, timestep:timestep });
67+ }
68+
69+ function clearAsyncCallbacks() {
70+ asyncCallbacks.forEach(function(timer) {
71+ if (!timer.executed) {
72+ clearTimeout(timer.timerId);
73+ }
74+ });
75+ }
76+
77+ function clearRecordedEvents() {
78+ recordedEvents = [];
79+ }
80+
81+ function getRecordedEventsLength() {
82+ return recordedEvents.length;
83+ }
84+
85+ function getRecordedEventAt(i) {
86+ return recordedEvents[i];
87+ }
88+
89+ function copyRecordedEvents() {
90+ return recordedEvents.slice();
91+ }
92+
93+ function loadRecordedEvents(events) {
94+ recordedEvents = events.slice();
95+ }
96+
97+ function setPaused(v) {
98+ programPaused = v;
99+ if (programPaused) {
100+ clearAsyncCallbacks();
101+ tracePath.stopRecording();
102+ }
103+ else {
104+ // executeCallbacks should only be called when continuing, not when restarting.
105+ //executeCallbacks(asyncCallbacks, false);
106+ tracePath.startRecording();
107+ }
108+ }
109+
110+ function getPaused() {
111+ return programPaused;
112+ }
113+
114+ // runtime is the prototype of wrappedRuntime
115+ // so we can access all runtime properties too
116+ var wrappedRuntime = Object.create(runtime);
117+ wrappedRuntime.notify = wrapNotify;
118+ wrappedRuntime.runDelayed = wrapRunDelayed;
119+
120+ var assignedPropTracker = Object.create(wrappedRuntime);
121+ var moduleInstance = module.make(assignedPropTracker);
122+ // The main module stores imported modules onto the runtime.
123+ // To ensure only one instance of each module is created,
124+ // we assign them back on the original runtime object.
125+ Object.keys(assignedPropTracker).forEach(function(key) {
126+ runtime[key] = assignedPropTracker[key];
127+ });
128+
129+ var moduleNodes = flattenNodes(wrappedRuntime.inputs);
130+ var tracePath = tracePathInit(runtime, moduleInstance.main);
131+
132+ return {
133+ moduleInstance: moduleInstance,
134+ moduleNodes: moduleNodes,
135+ initialNodeValues: saveNodeValues(moduleNodes),
136+ initialAsyncCallbacks: asyncCallbacks.slice(),
137+ // API functions
138+ clearAsyncCallbacks: clearAsyncCallbacks,
139+ clearRecordedEvents: clearRecordedEvents,
140+ getRecordedEventsLength: getRecordedEventsLength,
141+ getRecordedEventAt: getRecordedEventAt,
142+ copyRecordedEvents: copyRecordedEvents,
143+ loadRecordedEvents: loadRecordedEvents,
144+ getPaused: getPaused,
145+ setPaused: setPaused,
146+ tracePath: tracePath
147+ };
148+}
149+
150+function debuggerInit(debugModule, runtime, hotSwapState /* =undefined */) {
151+ var eventCounter = 0;
152+
153+ function resetProgram() {
154+ debugModule.clearAsyncCallbacks();
155+ restoreNodeValues(debugModule.moduleNodes, debugModule.initialNodeValues);
156+ redrawGraphics();
157+ }
158+
159+ function restartProgram() {
160+ resetProgram();
161+ debugModule.tracePath.clearTraces();
162+ debugModule.clearRecordedEvents();
163+ debugModule.setPaused(false);
164+ executeCallbacks(debugModule.initialAsyncCallbacks, true);
165+ }
166+
167+ function pauseProgram() {
168+ debugModule.setPaused(true);
169+ eventCounter = debugModule.getRecordedEventsLength();
170+ }
171+
172+ function continueProgram() {
173+ if (debugModule.getPaused())
174+ {
175+ debugModule.setPaused(false);
176+ }
177+ }
178+
179+ function stepTo(index) {
180+ if (!debugModule.getPaused()) {
181+ debugModule.setPaused(true);
182+ resetProgram();
183+ }
184+
185+ if (index < 0 || index > getMaxSteps()) {
186+ throw "Index out of bounds: " + index;
187+ }
188+
189+ if (index < eventCounter) {
190+ resetProgram();
191+ eventCounter = 0;
192+ }
193+
194+ assert(index >= eventCounter, "index must be bad");
195+ while (eventCounter < index) {
196+ var nextEvent = debugModule.getRecordedEventAt(eventCounter);
197+ runtime.notify(nextEvent.id, nextEvent.value, nextEvent.timestep);
198+
199+ eventCounter += 1;
200+ }
201+ assert(eventCounter == index, "while loop didn't work");
202+ }
203+
204+ function getMaxSteps() {
205+ return debugModule.getRecordedEventsLength();
206+ }
207+
208+ function doPlayback(eventList) {
209+ var x = eventList.shift();
210+ var time = x[2];
211+ runtime.notify(x[0], x[1], x[2]);
212+
213+ if (eventList.length > 0) {
214+ var delta = eventList[0][2] - time;
215+ setTimeout(function() { doPlayback(eventList); }, delta);
216+ }
217+ }
218+
219+ function redrawGraphics() {
220+ var graphicsNode = debugModule.moduleInstance.main.kids[0];
221+ graphicsNode.recv(Date.now(), true, debugModule.moduleInstance.main.id);
222+ }
223+
224+ function getHotSwapState() {
225+ var counter = eventCounter;
226+ if (!debugModule.getPaused()) {
227+ counter = getMaxSteps();
228+ }
229+ return {
230+ recordedEvents: debugModule.copyRecordedEvents(),
231+ eventCounter: counter
232+ };
233+ }
234+
235+ function dispose() {
236+ var parentNode = runtime.node.parentNode;
237+ parentNode.removeChild(debugModule.tracePath.canvas);
238+ parentNode.removeChild(runtime.node);
239+ }
240+
241+ if (hotSwapState) {
242+ debugModule.setPaused(true);
243+ debugModule.loadRecordedEvents(hotSwapState.recordedEvents);
244+
245+ // draw new trace path
246+ debugModule.tracePath.startRecording();
247+ stepTo(getMaxSteps());
248+ debugModule.tracePath.stopRecording();
249+
250+ stepTo(hotSwapState.eventCounter);
251+ }
252+
253+ runtime.node.parentNode.appendChild(debugModule.tracePath.canvas);
254+
255+ var elmDebugger = {
256+ restart: restartProgram,
257+ pause: pauseProgram,
258+ kontinue: continueProgram,
259+ getMaxSteps: getMaxSteps,
260+ stepTo: stepTo,
261+ getPaused: debugModule.getPaused,
262+ getHotSwapState: getHotSwapState,
263+ dispose: dispose,
264+ allNodes: debugModule.moduleNodes,
265+ mainNode: debugModule.moduleInstance.main
266+ };
267+
268+ return elmDebugger;
269+}
270+
271+function Point(x, y) {
272+ this.x = x;
273+ this.y = y;
274+
275+ this.translate = function(x, y) {
276+ return new Point(this.x + x, this.y + y);
277+ }
278+
279+ this.equals = function(p) {
280+ return this.x == p.x && this.y == p.y;
281+ }
282+}
283+
284+function tracePathInit(runtime, mainNode) {
285+ var List = Elm.List.make(runtime);
286+ var Signal = Elm.Signal.make(runtime);
287+ var tracePathNode = A2(Signal.lift, graphicsUpdate, mainNode);
288+ var tracePathCanvas = createCanvas();
289+ var tracePositions = {};
290+ var recordingTraces = true;
291+
292+ function findPositions(currentScene) {
293+ var positions = {};
294+ function processElement(elem, offset) {
295+ if (elem.element.ctor == "Custom" && elem.element.type == "Collage")
296+ {
297+ List.map(F2(processForm)(offset))(elem.element.model.forms);
298+ }
299+ else if (elem.element.ctor == "Image" && elem.props.debugTracePathId)
300+ {
301+ positions[elem.props.debugTracePathId] = new Point(offset.x, offset.y);
302+ }
303+ }
304+
305+ function processForm(offset, form) {
306+ if (form.form.ctor == "FElement")
307+ {
308+ processElement(form.form._0, offset.translate(form.x, -form.y));
309+ }
310+ }
311+
312+ processElement(currentScene, new Point(0, 0));
313+ return positions;
314+ }
315+
316+ function appendPositions(positions) {
317+ for (var id in positions) {
318+ var pos = positions[id];
319+ if (tracePositions.hasOwnProperty(id)) {
320+ var points = tracePositions[id];
321+ if (!pos.equals(points[points.length-1])) {
322+ points.push(pos);
323+ }
324+ }
325+ else {
326+ tracePositions[id] = [pos];
327+ }
328+ }
329+ }
330+
331+ function graphicsUpdate(currentScene) {
332+ if (!recordingTraces) {
333+ return;
334+ }
335+
336+ var ctx = tracePathCanvas.getContext('2d');
337+ ctx.clearRect(0, 0, tracePathCanvas.width, tracePathCanvas.height);
338+
339+ ctx.save();
340+ ctx.translate(ctx.canvas.width/2, ctx.canvas.height/2);
341+ appendPositions(findPositions(currentScene));
342+ for (var id in tracePositions)
343+ {
344+ ctx.beginPath();
345+ var points = tracePositions[id];
346+ for (var i=0; i < points.length; i++)
347+ {
348+ var p = points[i];
349+ if (i == 0) {
350+ ctx.moveTo(p.x, p.y);
351+ }
352+ else {
353+ ctx.lineTo(p.x, p.y);
354+ }
355+ }
356+ ctx.lineWidth = 1;
357+ ctx.strokeStyle = "#888a85";
358+ ctx.stroke();
359+
360+ for (var i=0; i < points.length; i++)
361+ {
362+ var p = points[i];
363+ ctx.beginPath();
364+ ctx.arc(p.x, p.y, 2, 0, Math.PI * 2);
365+ ctx.fillStyle = "#2e3436";
366+ ctx.fill();
367+ }
368+ }
369+
370+ ctx.restore();
371+ }
372+
373+ function clearTraces() {
374+ tracePositions = {};
375+ }
376+
377+ function stopRecording() {
378+ recordingTraces = false;
379+ }
380+
381+ function startRecording() {
382+ recordingTraces = true;
383+ }
384+
385+ return {
386+ graphicsUpdate: graphicsUpdate,
387+ canvas: tracePathCanvas,
388+ clearTraces: clearTraces,
389+ stopRecording: stopRecording,
390+ startRecording: startRecording
391+ }
392+}
393+
394+
395+function executeCallbacks(callbacks, reexecute) {
396+ callbacks.forEach(function(timer) {
397+ if (reexecute || !timer.executed) {
398+ var func = timer.func;
399+ func();
400+ }
401+ });
402+}
403+
404+function createCanvas() {
405+ var c = document.createElement('canvas');
406+ c.width = window.innerWidth;
407+ c.height = window.innerHeight;
408+ c.style.position = "absolute";
409+ c.style.top = "0";
410+ c.style.left = "0";
411+ return c;
412+}
413+
414+function assert(bool, msg) {
415+ if (!bool) {
416+ throw "Assertion error: " + msg;
417+ }
418+}
419+
420+function saveNodeValues(allNodes) {
421+ var nodeValues = [];
422+
423+ allNodes.forEach(function(node) {
424+ nodeValues.push({ value: node.value, id: node.id });
425+ });
426+
427+ return nodeValues;
428+};
429+
430+
431+function restoreNodeValues(allNodes, nodeStates) {
432+ assert(allNodes.length == nodeStates.length, "saved program state has wrong length");
433+ for (var i=0; i < allNodes.length; i++) {
434+ var node = allNodes[i];
435+ var state = nodeStates[i];
436+ assert(node.id == state.id, "the nodes moved position");
437+
438+ node.value = state.value;
439+ }
440+}
441+
442+function flattenNodes(nodes) {
443+ var nodesById = {};
444+
445+ function addAllToDict(node) {
446+ nodesById[node.id] = node;
447+ node.kids.forEach(addAllToDict);
448+ }
449+ nodes.forEach(addAllToDict);
450+
451+ var allNodes = Object.keys(nodesById).sort().map(function(key) {
452+ return nodesById[key];
453+ });
454+ return allNodes;
455+};
456+
457+}());
runtime/Init.js+10−1
@@ -84,6 +84,11 @@ function init(display, container, module, ports, moduleToReplace) {
8484 return newElm;
8585 }
8686
87+ function dispose() {
88+ removeListeners(listeners);
89+ inputs = [];
90+ }
91+
8792 var Module = {};
8893 var reportAnyErrors = function() {};
8994 try {
@@ -110,7 +115,11 @@ function init(display, container, module, ports, moduleToReplace) {
110115 }
111116
112117 reportAnyErrors();
113− return { swap:swap, ports:elm.ports.outgoing };
118+ return {
119+ swap:swap,
120+ ports:elm.ports.outgoing,
121+ dispose:dispose
122+ };
114123};
115124
116125function checkPorts(elm) {