on 38b9301 help sign in
elk/runtime/Debugger.js
323 lines 7.6 kB history raw
2(function() {
3'use strict';
5Elm.Debugger = null;
6Elm.debuggerAttach = function(module) {
7 return {
8 make: function(runtime) {
9 var wrappedModule = debugModule(module, runtime);
10 Elm.debuggerInit = function() {
11 Elm.Debugger = debuggerInit(wrappedModule, runtime);
12 };
13 return wrappedModule.moduleInstance;
14 }
15 };
16};
18function debugModule(module, runtime) {
19 var programPaused = false;
20 var recordedEvents = [];
21 var eventCounter = 0;
22 var asyncTimers = [];
24 function wrapNotify(id, v) {
25 var timestep = Date.now();
27 if (programPaused) {
28 // ignore notify because we are stepping
29 }
30 else {
31 runtime.notify(id, v, timestep);
32 recordEvent(id, v, timestep);
33 }
34 };
36 function wrapRunDelayed(func, delayMs) {
37 return safeSetTimeout(func, delayMs);
38 };
40 function safeSetTimeout(func, delayMs) {
41 var timerId = setTimeout(func, delayMs);
42 asyncTimers.push({ func:func, delayMs:delayMs, timerId:timerId });
43 return timerId;
44 }
46 function recordEvent(id, v, timestep) {
47 recordedEvents.push({ id:id, value:v, timestep:timestep });
48 }
50 function clearAsyncEvents() {
51 asyncTimers.forEach(function(timer) {
52 clearTimeout(timer.timerId);
53 });
54 }
56 function clearRecordedEvents() {
57 recordedEvents = [];
58 }
60 function getRecordedEvents() {
61 return recordedEvents.slice();
62 }
64 function setPaused(v) {
65 programPaused = v;
66 }
68 function getPaused() {
69 return programPaused;
70 }
72 var wrappedRuntime = {
73 notify: wrapNotify,
74 runDelayed: wrapRunDelayed,
75 node: runtime.node,
76 display: runtime.display,
77 id: runtime.id,
78 addListener: runtime.addListener,
79 inputs: runtime.inputs
80 };
82 var moduleInstance = module.make(wrappedRuntime);
83 var moduleNodes = flattenNodes(wrappedRuntime.inputs);
85 return {
86 moduleInstance: moduleInstance,
87 moduleNodes: moduleNodes,
88 initialNodeValues: saveNodeValues(moduleNodes),
89 initialAsyncTimers: asyncTimers.slice(),
90 // API functions
91 clearAsyncEvents: clearAsyncEvents,
92 clearRecordedEvents: clearRecordedEvents,
93 getRecordedEvents: getRecordedEvents,
94 getPaused: getPaused,
95 setPaused: setPaused
96 };
99function debuggerInit(debugModule, runtime) {
100 var Signal = Elm.Signal.make(runtime);
101 var graphicsNode = debugModule.moduleInstance.main.kids[0];
103 var tracePath = tracePathInit(runtime);
104 var tracePathNode = A2(Signal.lift, tracePath.graphicsUpdate, graphicsNode);
105 runtime.node.parentNode.appendChild(tracePath.canvas);
107 function resetProgram() {
108 debugModule.clearAsyncEvents();
109 restoreNodeValues(debugModule.moduleNodes, debugModule.initialNodeValues);
110 redrawGraphics();
111 }
113 function restartProgram() {
114 resetProgram();
115 debugModule.clearRecordedEvents();
116 debugModule.initialAsyncTimers.forEach(function(timer) {
117 var func = timer.func;
118 func();
119 });
120 }
122 function stepTo(index) {
123 if (!debugModule.getPaused()) {
124 debugModule.setPaused(true);
125 resetProgram();
126 }
128 if (index < 0 || index >= recordedEvents.length) {
129 throw "Index out of bounds: " + index;
130 }
132 if (index < eventCounter) {
133 resetProgram();
134 eventCounter = 0;
135 }
137 assert(index >= eventCounter, "index must be bad");
138 while (eventCounter < index) {
139 var nextEvent = recordedEvents[eventCounter];
140 runtime.notify(nextEvent.id, nextEvent.value, nextEvent.timestep);
142 eventCounter += 1;
143 }
144 assert(eventCounter == index, "while loop didnt' work");
145 }
147 function getMaxSteps() {
148 return recordedEvents.length - 1;
149 }
151 function doPlayback(eventList) {
152 var x = eventList.shift();
153 var time = x[2];
154 runtime.notify(x[0], x[1], x[2]);
156 if (eventList.length > 0) {
157 var delta = eventList[0][2] - time;
158 setTimeout(function() { doPlayback(eventList); }, delta);
159 }
160 }
162 function redrawGraphics() {
163 graphicsNode.recv(Date.now(), true, debugModule.moduleInstance.main.id);
164 }
166 var elmDebugger = {
167 reset: resetProgram,
168 restart: restartProgram,
169 getMaxSteps: getMaxSteps,
170 stepTo: stepTo
171 };
173 return elmDebugger;
176function Point(x, y) {
177 this.x = x;
178 this.y = y;
180 this.translate = function(x, y) {
181 return new Point(this.x + x, this.y + y);
182 }
184 this.equals = function(p) {
185 return this.x == p.x && this.y == p.y;
186 }
189function tracePathInit(runtime) {
190 var List = Elm.List.make(runtime);
191 var tracePathCanvas = createCanvas();
192 var tracePositions = {};
194 function findPositions(currentScene) {
195 var positions = {};
196 function processElement(elem, offset) {
197 if (elem.element.ctor == "Custom" && elem.element.type == "Collage")
198 {
199 List.map(F2(processForm)(offset))(elem.element.model.forms);
200 }
201 else if (elem.element.ctor == "Image" && elem.props.debugTracePathId)
202 {
203 positions[elem.props.debugTracePathId] = new Point(offset.x, offset.y);
204 }
205 }
207 function processForm(offset, form) {
208 if (form.form.ctor == "FElement")
209 {
210 processElement(form.form._0, offset.translate(form.x, -form.y));
211 }
212 }
214 processElement(currentScene, new Point(0, 0));
215 return positions;
216 }
218 function appendPositions(positions) {
219 for (var id in positions) {
220 var pos = positions[id];
221 if (tracePositions.hasOwnProperty(id)) {
222 var points = tracePositions[id];
223 if (!pos.equals(points[points.length-1])) {
224 points.push(pos);
225 }
226 }
227 else {
228 tracePositions[id] = [pos];
229 }
230 }
231 }
233 function graphicsUpdate(currentScene) {
234 var ctx = tracePathCanvas.getContext('2d');
235 ctx.clearRect(0, 0, tracePathCanvas.width, tracePathCanvas.height);
236 ctx.fillText("Debugger trace path", 20, 20);
238 ctx.save();
239 ctx.translate(ctx.canvas.width/2, ctx.canvas.height/2);
240 appendPositions(findPositions(currentScene));
241 for (var id in tracePositions)
242 {
243 ctx.beginPath();
244 var points = tracePositions[id];
245 for (var i=0; i < points.length; i++)
246 {
247 var p = points[i];
248 if (i == 0) {
249 ctx.moveTo(p.x, p.y);
250 }
251 else {
252 ctx.lineTo(p.x, p.y);
253 }
254 }
255 }
256 ctx.lineWidth = 1;
257 ctx.strokeStyle = "black";
258 ctx.stroke();
260 ctx.restore();
261 }
263 function clearTraces() {
264 tracePositions = {};
265 }
267 return {
268 graphicsUpdate: graphicsUpdate,
269 canvas: tracePathCanvas,
270 clearTraces: clearTraces
271 }
275function createCanvas() {
276 var c = document.createElement('canvas');
277 c.width = window.innerWidth;
278 c.height = window.innerHeight;
279 c.style.position = "absolute";
280 c.style.top = "0";
281 c.style.left = "0";
282 return c;
285function assert(bool, msg) {
286 if (!bool) {
287 throw "Assertion error: " + msg;
288 }
291function saveNodeValues(allNodes) {
292 var nodeValues = [];
294 allNodes.forEach(function(node) {
295 nodeValues.push({ value: node.value, id: node.id });
296 });
298 return nodeValues;
299};
302function restoreNodeValues(allNodes, nodeStates) {
303 assert(allNodes.length == nodeStates.length, "saved program state has wrong length");
304 for (var i=0; i < allNodes.length; i++) {
305 var node = allNodes[i];
306 var state = nodeStates[i];
307 assert(node.id == state.id, "the nodes moved position");
309 node.value = state.value;
310 }
313function flattenNodes(nodes) {
314 var treeReduce = function(prev, cur, i, arr) {
315 var flatKids = cur.kids.reduce(treeReduce, []);
316 flatKids.push(cur);
317 return prev.concat(flatKids);
318 }
320 return nodes.reduce(treeReduce, []);
321};
323}());