on 403f86a help sign in
elk/runtime/Debugger.js
391 lines 9.4 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.Debugger = debuggerInit(wrappedModule, runtime);
11 if (parent.window) {
12 var dispatch = function(_) {
13 parent.window.postMessage("elmDebuggerInit", window.location.origin);
14 };
15 if (parent.window.document.readyState === "complete") {
16 dispatch();
17 }
18 else {
19 parent.window.addEventListener("load", dispatch);
20 }
21 }
22 return wrappedModule.moduleInstance;
23 }
24 };
25};
27function debugModule(module, runtime) {
28 var programPaused = false;
29 var recordedEvents = [];
30 var asyncCallbacks = [];
32 function wrapNotify(id, v) {
33 var timestep = Date.now();
35 if (programPaused) {
36 // ignore async events generated while playing back
37 // or user events while program is paused
38 }
39 else {
40 runtime.notify(id, v, timestep);
41 recordEvent(id, v, timestep);
42 if (parent.window) {
43 parent.window.postMessage("elmNotify", window.location.origin);
44 }
45 }
46 };
48 function wrapRunDelayed(func, delayMs) {
49 var cbObj = { func:func, delayMs:delayMs, timerId:0, executed:false };
50 var timerId = setTimeout(function() {
51 cbObj.executed = true;
52 func();
53 }, delayMs);
54 cbObj.timerId = timerId;
55 asyncCallbacks.push(cbObj);
56 return timerId;
57 }
59 function recordEvent(id, v, timestep) {
60 recordedEvents.push({ id:id, value:v, timestep:timestep });
61 }
63 function clearAsyncCallbacks() {
64 asyncCallbacks.forEach(function(timer) {
65 if (!timer.executed) {
66 clearTimeout(timer.timerId);
67 }
68 });
69 }
71 function clearRecordedEvents() {
72 recordedEvents = [];
73 }
75 function getRecordedEventsLength() {
76 return recordedEvents.length;
77 }
79 function getRecordedEventAt(i) {
80 return recordedEvents[i];
81 }
83 function setPaused(v) {
84 programPaused = v;
85 if (programPaused) {
86 clearAsyncCallbacks();
87 tracePath.stopRecording();
88 }
89 else {
90 // executeCallbacks should only be called when continuing, not when restarting.
91 //executeCallbacks(asyncCallbacks, false);
92 tracePath.startRecording();
93 }
94 }
96 function getPaused() {
97 return programPaused;
98 }
100 var wrappedRuntime = {
101 notify: wrapNotify,
102 runDelayed: wrapRunDelayed,
103 node: runtime.node,
104 display: runtime.display,
105 id: runtime.id,
106 addListener: runtime.addListener,
107 inputs: runtime.inputs
108 };
110 var moduleInstance = module.make(wrappedRuntime);
111 var moduleNodes = flattenNodes(wrappedRuntime.inputs);
112 var tracePath = tracePathInit(runtime);
114 return {
115 moduleInstance: moduleInstance,
116 moduleNodes: moduleNodes,
117 initialNodeValues: saveNodeValues(moduleNodes),
118 initialAsyncCallbacks: asyncCallbacks.slice(),
119 // API functions
120 clearAsyncCallbacks: clearAsyncCallbacks,
121 clearRecordedEvents: clearRecordedEvents,
122 getRecordedEventsLength: getRecordedEventsLength,
123 getRecordedEventAt: getRecordedEventAt,
124 getPaused: getPaused,
125 setPaused: setPaused,
126 tracePath: tracePath
127 };
130function debuggerInit(debugModule, runtime) {
131 var Signal = Elm.Signal.make(runtime);
133 var tracePathNode = A2(Signal.lift, debugModule.tracePath.graphicsUpdate, debugModule.moduleInstance.main);
134 runtime.node.parentNode.appendChild(debugModule.tracePath.canvas);
136 var eventCounter = 0;
138 function resetProgram() {
139 debugModule.clearAsyncCallbacks();
140 restoreNodeValues(debugModule.moduleNodes, debugModule.initialNodeValues);
141 redrawGraphics();
142 }
144 function restartProgram() {
145 resetProgram();
146 debugModule.tracePath.clearTraces();
147 debugModule.clearRecordedEvents();
148 debugModule.setPaused(false);
149 executeCallbacks(debugModule.initialAsyncCallbacks, true);
150 }
152 function pauseProgram() {
153 debugModule.setPaused(true);
154 eventCounter = debugModule.getRecordedEventsLength();
155 }
157 function continueProgram() {
158 if (debugModule.getPaused())
159 {
160 debugModule.setPaused(false);
161 }
162 }
164 function stepTo(index) {
165 if (!debugModule.getPaused()) {
166 debugModule.setPaused(true);
167 resetProgram();
168 }
170 if (index < 0 || index > getMaxSteps()) {
171 throw "Index out of bounds: " + index;
172 }
174 if (index < eventCounter) {
175 resetProgram();
176 eventCounter = 0;
177 }
179 assert(index >= eventCounter, "index must be bad");
180 while (eventCounter < index) {
181 var nextEvent = debugModule.getRecordedEventAt(eventCounter);
182 runtime.notify(nextEvent.id, nextEvent.value, nextEvent.timestep);
184 eventCounter += 1;
185 }
186 assert(eventCounter == index, "while loop didn't work");
187 }
189 function getMaxSteps() {
190 return debugModule.getRecordedEventsLength();
191 }
193 function doPlayback(eventList) {
194 var x = eventList.shift();
195 var time = x[2];
196 runtime.notify(x[0], x[1], x[2]);
198 if (eventList.length > 0) {
199 var delta = eventList[0][2] - time;
200 setTimeout(function() { doPlayback(eventList); }, delta);
201 }
202 }
204 function redrawGraphics() {
205 var graphicsNode = debugModule.moduleInstance.main.kids[0];
206 graphicsNode.recv(Date.now(), true, debugModule.moduleInstance.main.id);
207 }
209 var elmDebugger = {
210 restart: restartProgram,
211 pause: pauseProgram,
212 kontinue: continueProgram,
213 getMaxSteps: getMaxSteps,
214 stepTo: stepTo,
215 getPaused: debugModule.getPaused
216 };
218 return elmDebugger;
221function Point(x, y) {
222 this.x = x;
223 this.y = y;
225 this.translate = function(x, y) {
226 return new Point(this.x + x, this.y + y);
227 }
229 this.equals = function(p) {
230 return this.x == p.x && this.y == p.y;
231 }
234function tracePathInit(runtime) {
235 var List = Elm.List.make(runtime);
236 var tracePathCanvas = createCanvas();
237 var tracePositions = {};
238 var recordingTraces = true;
240 function findPositions(currentScene) {
241 var positions = {};
242 function processElement(elem, offset) {
243 if (elem.element.ctor == "Custom" && elem.element.type == "Collage")
244 {
245 List.map(F2(processForm)(offset))(elem.element.model.forms);
246 }
247 else if (elem.element.ctor == "Image" && elem.props.debugTracePathId)
248 {
249 positions[elem.props.debugTracePathId] = new Point(offset.x, offset.y);
250 }
251 }
253 function processForm(offset, form) {
254 if (form.form.ctor == "FElement")
255 {
256 processElement(form.form._0, offset.translate(form.x, -form.y));
257 }
258 }
260 processElement(currentScene, new Point(0, 0));
261 return positions;
262 }
264 function appendPositions(positions) {
265 for (var id in positions) {
266 var pos = positions[id];
267 if (tracePositions.hasOwnProperty(id)) {
268 var points = tracePositions[id];
269 if (!pos.equals(points[points.length-1])) {
270 points.push(pos);
271 }
272 }
273 else {
274 tracePositions[id] = [pos];
275 }
276 }
277 }
279 function graphicsUpdate(currentScene) {
280 if (!recordingTraces) {
281 return;
282 }
284 var ctx = tracePathCanvas.getContext('2d');
285 ctx.clearRect(0, 0, tracePathCanvas.width, tracePathCanvas.height);
287 ctx.save();
288 ctx.translate(ctx.canvas.width/2, ctx.canvas.height/2);
289 appendPositions(findPositions(currentScene));
290 for (var id in tracePositions)
291 {
292 ctx.beginPath();
293 var points = tracePositions[id];
294 for (var i=0; i < points.length; i++)
295 {
296 var p = points[i];
297 if (i == 0) {
298 ctx.moveTo(p.x, p.y);
299 }
300 else {
301 ctx.lineTo(p.x, p.y);
302 }
303 }
304 }
305 ctx.lineWidth = 1;
306 ctx.strokeStyle = "black";
307 ctx.stroke();
309 ctx.restore();
310 }
312 function clearTraces() {
313 tracePositions = {};
314 }
316 function stopRecording() {
317 recordingTraces = false;
318 }
320 function startRecording() {
321 recordingTraces = true;
322 }
324 return {
325 graphicsUpdate: graphicsUpdate,
326 canvas: tracePathCanvas,
327 clearTraces: clearTraces,
328 stopRecording: stopRecording,
329 startRecording: startRecording
330 }
334function executeCallbacks(callbacks, reexecute) {
335 callbacks.forEach(function(timer) {
336 if (reexecute || !timer.executed) {
337 var func = timer.func;
338 func();
339 }
340 });
343function createCanvas() {
344 var c = document.createElement('canvas');
345 c.width = window.innerWidth;
346 c.height = window.innerHeight;
347 c.style.position = "absolute";
348 c.style.top = "0";
349 c.style.left = "0";
350 return c;
353function assert(bool, msg) {
354 if (!bool) {
355 throw "Assertion error: " + msg;
356 }
359function saveNodeValues(allNodes) {
360 var nodeValues = [];
362 allNodes.forEach(function(node) {
363 nodeValues.push({ value: node.value, id: node.id });
364 });
366 return nodeValues;
367};
370function restoreNodeValues(allNodes, nodeStates) {
371 assert(allNodes.length == nodeStates.length, "saved program state has wrong length");
372 for (var i=0; i < allNodes.length; i++) {
373 var node = allNodes[i];
374 var state = nodeStates[i];
375 assert(node.id == state.id, "the nodes moved position");
377 node.value = state.value;
378 }
381function flattenNodes(nodes) {
382 var treeReduce = function(prev, cur, i, arr) {
383 var flatKids = cur.kids.reduce(treeReduce, []);
384 flatKids.push(cur);
385 return prev.concat(flatKids);
386 }
388 return nodes.reduce(treeReduce, []);
389};
391}());