on 44d2c72 help sign in
elk/runtime/Debugger.js
457 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 wrappedRuntime.notify = wrapNotify;
118 wrappedRuntime.runDelayed = wrapRunDelayed;
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 });
129 var moduleNodes = flattenNodes(wrappedRuntime.inputs);
130 var tracePath = tracePathInit(runtime, moduleInstance.main);
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 };
150function debuggerInit(debugModule, runtime, hotSwapState /* =undefined */) {
151 var eventCounter = 0;
153 function resetProgram() {
154 debugModule.clearAsyncCallbacks();
155 restoreNodeValues(debugModule.moduleNodes, debugModule.initialNodeValues);
156 redrawGraphics();
157 }
159 function restartProgram() {
160 resetProgram();
161 debugModule.tracePath.clearTraces();
162 debugModule.clearRecordedEvents();
163 debugModule.setPaused(false);
164 executeCallbacks(debugModule.initialAsyncCallbacks, true);
165 }
167 function pauseProgram() {
168 debugModule.setPaused(true);
169 eventCounter = debugModule.getRecordedEventsLength();
170 }
172 function continueProgram() {
173 if (debugModule.getPaused())
174 {
175 debugModule.setPaused(false);
176 }
177 }
179 function stepTo(index) {
180 if (!debugModule.getPaused()) {
181 debugModule.setPaused(true);
182 resetProgram();
183 }
185 if (index < 0 || index > getMaxSteps()) {
186 throw "Index out of bounds: " + index;
187 }
189 if (index < eventCounter) {
190 resetProgram();
191 eventCounter = 0;
192 }
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);
199 eventCounter += 1;
200 }
201 assert(eventCounter == index, "while loop didn't work");
202 }
204 function getMaxSteps() {
205 return debugModule.getRecordedEventsLength();
206 }
208 function doPlayback(eventList) {
209 var x = eventList.shift();
210 var time = x[2];
211 runtime.notify(x[0], x[1], x[2]);
213 if (eventList.length > 0) {
214 var delta = eventList[0][2] - time;
215 setTimeout(function() { doPlayback(eventList); }, delta);
216 }
217 }
219 function redrawGraphics() {
220 var graphicsNode = debugModule.moduleInstance.main.kids[0];
221 graphicsNode.recv(Date.now(), true, debugModule.moduleInstance.main.id);
222 }
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 }
235 function dispose() {
236 var parentNode = runtime.node.parentNode;
237 parentNode.removeChild(debugModule.tracePath.canvas);
238 parentNode.removeChild(runtime.node);
239 }
241 if (hotSwapState) {
242 debugModule.setPaused(true);
243 debugModule.loadRecordedEvents(hotSwapState.recordedEvents);
245 // draw new trace path
246 debugModule.tracePath.startRecording();
247 stepTo(getMaxSteps());
248 debugModule.tracePath.stopRecording();
250 stepTo(hotSwapState.eventCounter);
251 }
253 runtime.node.parentNode.appendChild(debugModule.tracePath.canvas);
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 };
268 return elmDebugger;
271function Point(x, y) {
272 this.x = x;
273 this.y = y;
275 this.translate = function(x, y) {
276 return new Point(this.x + x, this.y + y);
277 }
279 this.equals = function(p) {
280 return this.x == p.x && this.y == p.y;
281 }
284function 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;
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 }
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 }
312 processElement(currentScene, new Point(0, 0));
313 return positions;
314 }
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 }
331 function graphicsUpdate(currentScene) {
332 if (!recordingTraces) {
333 return;
334 }
336 var ctx = tracePathCanvas.getContext('2d');
337 ctx.clearRect(0, 0, tracePathCanvas.width, tracePathCanvas.height);
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();
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 }
370 ctx.restore();
371 }
373 function clearTraces() {
374 tracePositions = {};
375 }
377 function stopRecording() {
378 recordingTraces = false;
379 }
381 function startRecording() {
382 recordingTraces = true;
383 }
385 return {
386 graphicsUpdate: graphicsUpdate,
387 canvas: tracePathCanvas,
388 clearTraces: clearTraces,
389 stopRecording: stopRecording,
390 startRecording: startRecording
391 }
395function executeCallbacks(callbacks, reexecute) {
396 callbacks.forEach(function(timer) {
397 if (reexecute || !timer.executed) {
398 var func = timer.func;
399 func();
400 }
401 });
404function 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;
414function assert(bool, msg) {
415 if (!bool) {
416 throw "Assertion error: " + msg;
417 }
420function saveNodeValues(allNodes) {
421 var nodeValues = [];
423 allNodes.forEach(function(node) {
424 nodeValues.push({ value: node.value, id: node.id });
425 });
427 return nodeValues;
428};
431function 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");
438 node.value = state.value;
439 }
442function flattenNodes(nodes) {
443 var nodesById = {};
445 function addAllToDict(node) {
446 nodesById[node.id] = node;
447 node.kids.forEach(addAllToDict);
448 }
449 nodes.forEach(addAllToDict);
451 var allNodes = Object.keys(nodesById).sort().map(function(key) {
452 return nodesById[key];
453 });
454 return allNodes;
455};
457}());