on e327ac0 help sign in
elk/runtime/Debugger.js
451 lines 11 kB history raw
2(function() {
3'use strict';
5Elm.Debugger = null;
6Elm.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};
17function 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 }
31function debugModule(module, runtime) {
32 var programPaused = false;
33 var recordedEvents = [];
34 var asyncCallbacks = [];
36 function wrapNotify(id, v) {
37 var timestep = Date.now();
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 };
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 }
65 function recordEvent(id, v, timestep) {
66 recordedEvents.push({ id:id, value:v, timestep:timestep });
67 }
69 function clearAsyncCallbacks() {
70 asyncCallbacks.forEach(function(timer) {
71 if (!timer.executed) {
72 clearTimeout(timer.timerId);
73 }
74 });
75 }
77 function clearRecordedEvents() {
78 recordedEvents = [];
79 }
81 function getRecordedEventsLength() {
82 return recordedEvents.length;
83 }
85 function getRecordedEventAt(i) {
86 return recordedEvents[i];
87 }
89 function copyRecordedEvents() {
90 return recordedEvents.slice();
91 }
93 function loadRecordedEvents(events) {
94 recordedEvents = events.slice();
95 }
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 }
110 function getPaused() {
111 return programPaused;
112 }
114 // runtime is the prototype of wrappedRuntime
115 // so we can access all runtime properties too
116 var wrappedRuntime = Object.create(runtime, {
117 notify: wrapNotify,
118 runDelayed: wrapRunDelayed
119 });
121 var moduleInstance = module.make(wrappedRuntime);
122 var moduleNodes = flattenNodes(wrappedRuntime.inputs);
123 var tracePath = tracePathInit(runtime);
125 return {
126 moduleInstance: moduleInstance,
127 moduleNodes: moduleNodes,
128 initialNodeValues: saveNodeValues(moduleNodes),
129 initialAsyncCallbacks: asyncCallbacks.slice(),
130 // API functions
131 clearAsyncCallbacks: clearAsyncCallbacks,
132 clearRecordedEvents: clearRecordedEvents,
133 getRecordedEventsLength: getRecordedEventsLength,
134 getRecordedEventAt: getRecordedEventAt,
135 copyRecordedEvents: copyRecordedEvents,
136 loadRecordedEvents: loadRecordedEvents,
137 getPaused: getPaused,
138 setPaused: setPaused,
139 tracePath: tracePath
140 };
143function debuggerInit(debugModule, runtime, hotSwapState /* =undefined */) {
144 var Signal = Elm.Signal.make(runtime);
146 var tracePathNode = A2(Signal.lift, debugModule.tracePath.graphicsUpdate, debugModule.moduleInstance.main);
147 runtime.node.parentNode.appendChild(debugModule.tracePath.canvas);
149 var eventCounter = 0;
151 function resetProgram() {
152 debugModule.clearAsyncCallbacks();
153 restoreNodeValues(debugModule.moduleNodes, debugModule.initialNodeValues);
154 redrawGraphics();
155 }
157 function restartProgram() {
158 resetProgram();
159 debugModule.tracePath.clearTraces();
160 debugModule.clearRecordedEvents();
161 debugModule.setPaused(false);
162 executeCallbacks(debugModule.initialAsyncCallbacks, true);
163 }
165 function pauseProgram() {
166 debugModule.setPaused(true);
167 eventCounter = debugModule.getRecordedEventsLength();
168 }
170 function continueProgram() {
171 if (debugModule.getPaused())
172 {
173 debugModule.setPaused(false);
174 }
175 }
177 function stepTo(index) {
178 if (!debugModule.getPaused()) {
179 debugModule.setPaused(true);
180 resetProgram();
181 }
183 if (index < 0 || index > getMaxSteps()) {
184 throw "Index out of bounds: " + index;
185 }
187 if (index < eventCounter) {
188 resetProgram();
189 eventCounter = 0;
190 }
192 assert(index >= eventCounter, "index must be bad");
193 while (eventCounter < index) {
194 var nextEvent = debugModule.getRecordedEventAt(eventCounter);
195 runtime.notify(nextEvent.id, nextEvent.value, nextEvent.timestep);
197 eventCounter += 1;
198 }
199 assert(eventCounter == index, "while loop didn't work");
200 }
202 function getMaxSteps() {
203 return debugModule.getRecordedEventsLength();
204 }
206 function doPlayback(eventList) {
207 var x = eventList.shift();
208 var time = x[2];
209 runtime.notify(x[0], x[1], x[2]);
211 if (eventList.length > 0) {
212 var delta = eventList[0][2] - time;
213 setTimeout(function() { doPlayback(eventList); }, delta);
214 }
215 }
217 function redrawGraphics() {
218 var graphicsNode = debugModule.moduleInstance.main.kids[0];
219 graphicsNode.recv(Date.now(), true, debugModule.moduleInstance.main.id);
220 }
222 function getHotSwapState() {
223 var counter = eventCounter;
224 if (!debugModule.getPaused()) {
225 counter = getMaxSteps();
226 }
227 return {
228 recordedEvents: debugModule.copyRecordedEvents(),
229 eventCounter: counter
230 };
231 }
233 function dispose() {
234 var parentNode = runtime.node.parentNode;
235 parentNode.removeChild(debugModule.tracePath.canvas);
236 parentNode.removeChild(runtime.node);
237 }
239 if (hotSwapState) {
240 debugModule.setPaused(true);
241 debugModule.loadRecordedEvents(hotSwapState.recordedEvents);
243 // draw new trace path
244 debugModule.tracePath.startRecording();
245 stepTo(getMaxSteps());
246 debugModule.tracePath.stopRecording();
248 stepTo(hotSwapState.eventCounter);
249 }
251 var elmDebugger = {
252 restart: restartProgram,
253 pause: pauseProgram,
254 kontinue: continueProgram,
255 getMaxSteps: getMaxSteps,
256 stepTo: stepTo,
257 getPaused: debugModule.getPaused,
258 getHotSwapState: getHotSwapState,
259 dispose: dispose,
260 allNodes: debugModule.moduleNodes,
261 mainNode: debugModule.moduleInstance.main
262 };
264 return elmDebugger;
267function Point(x, y) {
268 this.x = x;
269 this.y = y;
271 this.translate = function(x, y) {
272 return new Point(this.x + x, this.y + y);
273 }
275 this.equals = function(p) {
276 return this.x == p.x && this.y == p.y;
277 }
280function tracePathInit(runtime) {
281 var List = Elm.List.make(runtime);
282 var tracePathCanvas = createCanvas();
283 var tracePositions = {};
284 var recordingTraces = true;
286 function findPositions(currentScene) {
287 var positions = {};
288 function processElement(elem, offset) {
289 if (elem.element.ctor == "Custom" && elem.element.type == "Collage")
290 {
291 List.map(F2(processForm)(offset))(elem.element.model.forms);
292 }
293 else if (elem.element.ctor == "Image" && elem.props.debugTracePathId)
294 {
295 positions[elem.props.debugTracePathId] = new Point(offset.x, offset.y);
296 }
297 }
299 function processForm(offset, form) {
300 if (form.form.ctor == "FElement")
301 {
302 processElement(form.form._0, offset.translate(form.x, -form.y));
303 }
304 }
306 processElement(currentScene, new Point(0, 0));
307 return positions;
308 }
310 function appendPositions(positions) {
311 for (var id in positions) {
312 var pos = positions[id];
313 if (tracePositions.hasOwnProperty(id)) {
314 var points = tracePositions[id];
315 if (!pos.equals(points[points.length-1])) {
316 points.push(pos);
317 }
318 }
319 else {
320 tracePositions[id] = [pos];
321 }
322 }
323 }
325 function graphicsUpdate(currentScene) {
326 if (!recordingTraces) {
327 return;
328 }
330 var ctx = tracePathCanvas.getContext('2d');
331 ctx.clearRect(0, 0, tracePathCanvas.width, tracePathCanvas.height);
333 ctx.save();
334 ctx.translate(ctx.canvas.width/2, ctx.canvas.height/2);
335 appendPositions(findPositions(currentScene));
336 for (var id in tracePositions)
337 {
338 ctx.beginPath();
339 var points = tracePositions[id];
340 for (var i=0; i < points.length; i++)
341 {
342 var p = points[i];
343 if (i == 0) {
344 ctx.moveTo(p.x, p.y);
345 }
346 else {
347 ctx.lineTo(p.x, p.y);
348 }
349 }
350 ctx.lineWidth = 1;
351 ctx.strokeStyle = "#888a85";
352 ctx.stroke();
354 for (var i=0; i < points.length; i++)
355 {
356 var p = points[i];
357 ctx.beginPath();
358 ctx.arc(p.x, p.y, 2, 0, Math.PI * 2);
359 ctx.fillStyle = "#2e3436";
360 ctx.fill();
361 }
362 }
364 ctx.restore();
365 }
367 function clearTraces() {
368 tracePositions = {};
369 }
371 function stopRecording() {
372 recordingTraces = false;
373 }
375 function startRecording() {
376 recordingTraces = true;
377 }
379 return {
380 graphicsUpdate: graphicsUpdate,
381 canvas: tracePathCanvas,
382 clearTraces: clearTraces,
383 stopRecording: stopRecording,
384 startRecording: startRecording
385 }
389function executeCallbacks(callbacks, reexecute) {
390 callbacks.forEach(function(timer) {
391 if (reexecute || !timer.executed) {
392 var func = timer.func;
393 func();
394 }
395 });
398function createCanvas() {
399 var c = document.createElement('canvas');
400 c.width = window.innerWidth;
401 c.height = window.innerHeight;
402 c.style.position = "absolute";
403 c.style.top = "0";
404 c.style.left = "0";
405 return c;
408function assert(bool, msg) {
409 if (!bool) {
410 throw "Assertion error: " + msg;
411 }
414function saveNodeValues(allNodes) {
415 var nodeValues = [];
417 allNodes.forEach(function(node) {
418 nodeValues.push({ value: node.value, id: node.id });
419 });
421 return nodeValues;
422};
425function restoreNodeValues(allNodes, nodeStates) {
426 assert(allNodes.length == nodeStates.length, "saved program state has wrong length");
427 for (var i=0; i < allNodes.length; i++) {
428 var node = allNodes[i];
429 var state = nodeStates[i];
430 assert(node.id == state.id, "the nodes moved position");
432 node.value = state.value;
433 }
436function flattenNodes(nodes) {
437 var nodesById = {};
439 function addAllToDict(node) {
440 nodesById[node.id] = node;
441 node.kids.forEach(addAllToDict);
442 }
443 nodes.forEach(addAllToDict);
445 var allNodes = Object.keys(nodesById).sort().map(function(key) {
446 return nodesById[key];
447 });
448 return allNodes;
449};
451}());