on a4ea68e help sign in
elk/runtime/Debugger.js
620 lines 18 kB history raw
1(function() {
2'use strict';
4if (!window.location.origin) {
5 window.location.origin =
6 window.location.protocol + "//" +
7 window.location.hostname +
8 (window.location.port ? (':' + window.location.port) : '');
9}
11Elm.Debugger = null;
12Elm.debug = function(module, moduleFile, hotSwapState /* =undefined */) {
13 var mainHandle = Elm.fulscreen(debuggerAttach(module,hotSwapState));
14 mainHandle.fileName = moduleFile;
15 mainHandle.debug = Elm.Debugger;
16};
18function debuggerAttach(module, hotSwapState /* =undefined */) {
19 return {
20 make: function(runtime) {
21 var wrappedModule = debugModule(module, runtime);
22 Elm.Debugger = debuggerInit(wrappedModule, runtime, hotSwapState);
23 dispatchElmDebuggerInit();
24 return wrappedModule.debuggedModule;
25 }
26 };
27};
29var EVENTS_PER_SAVE = 100;
31function dispatchElmDebuggerInit() {
32 if (parent.window) {
33 var dispatch = function() {
34 parent.window.postMessage("elmDebuggerInit", window.location.origin);
35 };
36 if (parent.window.document.readyState === "complete") {
37 dispatch();
38 }
39 else {
40 parent.window.addEventListener("load", dispatch);
41 }
42 }
45function debugModule(module, runtime) {
46 var programPaused = false;
47 var recordedEvents = [];
48 var asyncCallbacks = [];
49 var snapshots = [];
50 var watchTracker = Elm.Native.Debug.make(runtime).watchTracker;
51 var pauseTime = 0;
52 var eventsUntilSnapshot = EVENTS_PER_SAVE;
53 runtime.debuggerStatus = runtime.debuggerStatus || {};
54 runtime.debuggerStatus.eventCounter = runtime.debuggerStatus.eventCounter || 0;
56 // runtime is the prototype of wrappedRuntime
57 // so we can access all runtime properties too
58 var wrappedRuntime = Object.create(runtime);
59 wrappedRuntime.notify = wrapNotify;
60 wrappedRuntime.runDelayed = wrapRunDelayed;
62 // make a copy of the wrappedRuntime
63 var assignedPropTracker = Object.create(wrappedRuntime);
64 var debuggedModule = module.make(assignedPropTracker);
66 // make sure the signal graph is actually a signal & extract the visual model
67 if ( !('recv' in debuggedModule.main) ) {
68 debuggedModule.main = Elm.Signal.make(runtime).constant(debuggedModule.main);
69 }
71 // The main module stores imported modules onto the runtime.
72 // To ensure only one instance of each module is created,
73 // we assign them back on the original runtime object.
74 Object.keys(assignedPropTracker).forEach(function(key) {
75 runtime[key] = assignedPropTracker[key];
76 });
78 var signalGraphNodes = flattenSignalGraph(wrappedRuntime.inputs);
79 var tracePath = tracePathInit(runtime, debuggedModule.main);
81 snapshots.push(snapshotSignalGraph(signalGraphNodes));
83 function wrapNotify(id, v) {
84 var timestep = runtime.timer.now();
86 if (programPaused) {
87 // ignore async events generated while playing back
88 // or user events while program is paused
89 return false;
90 }
91 else {
92 recordEvent(id, v, timestep);
93 var changed = runtime.notify(id, v, timestep);
94 snapshotOnCheckpoint();
95 if (parent.window) {
96 parent.window.postMessage("elmNotify", window.location.origin);
97 }
98 return changed;
99 }
100 };
102 function wrapRunDelayed(func, delayMs) {
103 if (programPaused) {
104 // Don't push timers and such to the callback stack while we're paused.
105 // It causes too many callbacks to be fired during unpausing.
106 return 0;
107 }
108 var cbObj = { func:func, delayMs:delayMs, timerId:0, executed:false };
109 var timerId = setTimeout(function() {
110 cbObj.executed = true;
111 func();
112 }, delayMs);
113 cbObj.timerId = timerId;
114 asyncCallbacks.push(cbObj);
115 return timerId;
116 }
118 function recordEvent(id, v, timestep) {
119 watchTracker.pushFrame();
120 recordedEvents.push({ id:id, value:v, timestep:timestep });
121 runtime.debuggerStatus.eventCounter += 1;
122 }
124 function clearAsyncCallbacks() {
125 asyncCallbacks.forEach(function(timer) {
126 if (!timer.executed) {
127 clearTimeout(timer.timerId);
128 }
129 });
130 }
132 function clearRecordedEvents() {
133 recordedEvents = [];
134 runtime.debuggerStatus.eventCounter = 0;
135 }
137 function getRecordedEventsLength() {
138 return recordedEvents.length;
139 }
141 function getRecordedEventAt(i) {
142 return recordedEvents[i];
143 }
145 function copyRecordedEvents() {
146 return recordedEvents.slice();
147 }
149 function loadRecordedEvents(events) {
150 recordedEvents = events.slice();
151 }
153 function clearSnapshots() {
154 snapshots = [snapshotSignalGraph(signalGraphNodes)];
155 }
157 function getSnapshotAt(i) {
158 var snapshotEvent = Math.floor(i / EVENTS_PER_SAVE);
159 assert(snapshotEvent < snapshots.length && snapshotEvent >= 0,
160 "Out of bounds index: " + snapshotEvent);
161 return snapshots[snapshotEvent];
162 }
164 function snapshotOnCheckpoint() {
165 if (eventsUntilSnapshot === 1) {
166 snapshots.push(snapshotSignalGraph(signalGraphNodes));
167 eventsUntilSnapshot = EVENTS_PER_SAVE;
168 } else {
169 eventsUntilSnapshot -= 1;
170 }
171 }
173 function setPaused() {
174 programPaused = true;
175 clearAsyncCallbacks();
176 pauseTime = Date.now();
177 tracePath.stopRecording();
178 preventInputEvents();
179 }
181 function setContinue(position) {
182 var pauseDelay = Date.now() - pauseTime;
183 runtime.timer.addDelay(pauseDelay);
184 programPaused = false;
186 // we need to dump the events that are ahead of where we're continuing.
187 var lastSnapshotPosition = Math.floor(position / EVENTS_PER_SAVE);
188 eventsUntilSnapshot = EVENTS_PER_SAVE - (position % EVENTS_PER_SAVE);
189 snapshots = snapshots.slice(0, lastSnapshotPosition + 1);
191 if (position < getRecordedEventsLength()) {
192 var lastEventTime = recordedEvents[position].timestep;
193 var scrubTime = runtime.timer.now() - lastEventTime;
194 runtime.timer.addDelay(scrubTime);
195 }
197 recordedEvents = recordedEvents.slice(0, position);
198 tracePath.clearTracesAfter(position);
199 runtime.debuggerStatus.eventCounter = position;
200 executeCallbacks(asyncCallbacks);
201 permitInputEvents();
203 tracePath.startRecording();
204 }
206 function getPaused() {
207 return programPaused;
208 }
210 function preventInputEvents(){
211 var events =
212 [ "click", "mousemove", "mouseup", "mousedown", "mouseclick"
213 , "keydown", "keypress", "keyup", "touchstart", "touchend"
214 , "touchcancel", "touchleave", "touchmove", "pointermove"
215 , "pointerdown", "pointerup", "pointerover", "pointerout"
216 , "pointerenter", "pointerleave", "pointercancel"
217 ];
219 var ignore = function(e) {
220 var evt = e ? e : window.event;
221 if (evt.stopPropagation) {
222 evt.stopPropagation();
223 }
224 if (evt.cancelBubble !== null) {
225 evt.cancelBubble = true;
226 }
227 if (evt.preventDefault) {
228 evt.preventDefault();
229 }
230 return false;
231 };
233 var ignoringDiv = document.getElementById("elmEventIgnorer");
234 if (!ignoringDiv) {
235 ignoringDiv = document.createElement("div");
236 ignoringDiv.id = "elmEventIgnorer";
237 ignoringDiv.style.position = "absolute";
238 ignoringDiv.style.top = "0px";
239 ignoringDiv.style.left = "0px";
240 ignoringDiv.style.width = "100%";
241 ignoringDiv.style.height = "100%";
243 for (var i = events.length; i-- ;) {
244 ignoringDiv.addEventListener(events[i], ignore, true);
245 }
246 runtime.node.appendChild(ignoringDiv);
247 }
248 }
250 function permitInputEvents(){
251 var ignoringDiv = document.getElementById("elmEventIgnorer");
252 ignoringDiv.parentNode.removeChild(ignoringDiv);
253 }
255 return {
256 debuggedModule: debuggedModule,
257 signalGraphNodes: signalGraphNodes,
258 initialSnapshot: snapshotSignalGraph(signalGraphNodes),
259 initialAsyncCallbacks: asyncCallbacks.slice(),
260 // API functions
261 clearAsyncCallbacks: clearAsyncCallbacks,
262 clearRecordedEvents: clearRecordedEvents,
263 getRecordedEventsLength: getRecordedEventsLength,
264 getRecordedEventAt: getRecordedEventAt,
265 copyRecordedEvents: copyRecordedEvents,
266 loadRecordedEvents: loadRecordedEvents,
267 clearSnapshots: clearSnapshots,
268 getSnapshotAt: getSnapshotAt,
269 snapshotOnCheckpoint: snapshotOnCheckpoint,
270 getPaused: getPaused,
271 setPaused: setPaused,
272 setContinue: setContinue,
273 tracePath: tracePath,
274 watchTracker: watchTracker
275 };
278// The debuggerHistory variable is passed in on hotswap. It represents
279// the a state of the debugger for it to assume during init. It contains
280// the paused state of the debugger, the recorded events, and the current
281// event being processed.
282function debuggerInit(debugModule, runtime, debuggerHistory /* =undefined */) {
283 var currentEventIndex = 0;
285 function resetProgram(position) {
286 var closestSnapshot = debugModule.getSnapshotAt(position);
287 debugModule.clearAsyncCallbacks();
288 restoreSnapshot(debugModule.signalGraphNodes, closestSnapshot);
289 redrawGraphics();
290 }
292 function restartProgram() {
293 pauseProgram();
294 resetProgram(0);
295 debugModule.watchTracker.clear();
296 debugModule.tracePath.clearTraces();
297 debugModule.clearRecordedEvents();
298 debugModule.clearSnapshots();
299 debugModule.setContinue(0);
300 executeCallbacks(debugModule.initialAsyncCallbacks);
301 }
303 function pauseProgram() {
304 debugModule.setPaused();
305 currentEventIndex = debugModule.getRecordedEventsLength();
306 }
308 function continueProgram() {
309 if (debugModule.getPaused())
310 {
311 var closestSnapshotIndex =
312 Math.floor(currentEventIndex / EVENTS_PER_SAVE) * EVENTS_PER_SAVE;
313 resetProgram(currentEventIndex);
314 var continueIndex = currentEventIndex;
315 currentEventIndex = closestSnapshotIndex;
316 stepTo(continueIndex);
317 debugModule.setContinue(currentEventIndex);
318 }
319 }
321 function stepTo(index) {
322 if (!debugModule.getPaused()) {
323 debugModule.setPaused();
324 resetProgram();
325 }
327 if (index < 0 || index > getMaxSteps()) {
328 throw "Index out of bounds: " + index;
329 }
331 if (index < currentEventIndex) {
332 var closestSnapshotIndex = Math.floor(index / EVENTS_PER_SAVE) * EVENTS_PER_SAVE;
333 resetProgram(index);
334 currentEventIndex = closestSnapshotIndex;
335 }
337 while (currentEventIndex < index) {
338 var nextEvent = debugModule.getRecordedEventAt(currentEventIndex);
339 runtime.notify(nextEvent.id, nextEvent.value, nextEvent.timestep);
341 currentEventIndex += 1;
342 }
343 }
345 function getMaxSteps() {
346 return debugModule.getRecordedEventsLength();
347 }
349 function redrawGraphics() {
350 var main = debugModule.debuggedModule.main
351 for (var i = main.kids.length ; i-- ; ) {
352 main.kids[i].recv(runtime.timer.now(), true, main.id);
353 }
354 }
356 function getHotSwapState() {
357 var continueIndex = currentEventIndex;
358 if (!debugModule.getPaused()) {
359 continueIndex = getMaxSteps();
360 }
361 return {
362 paused: debugModule.getPaused(),
363 recordedEvents: debugModule.copyRecordedEvents(),
364 currentEventIndex: continueIndex
365 };
366 }
368 function dispose() {
369 var parentNode = runtime.node.parentNode;
370 parentNode.removeChild(debugModule.tracePath.canvas);
371 parentNode.removeChild(runtime.node);
372 }
374 if (debuggerHistory) {
375 // The problem is that we want to previous paused state. But
376 // by the time JS reaches here, the old code has been swapped out
377 // and the new modules are being generated. So we can ask the
378 // debugging console what it thinks the pause state is and go
379 // from there.
380 var paused = debuggerHistory.paused;
381 debugModule.setPaused();
382 debugModule.loadRecordedEvents(debuggerHistory.recordedEvents);
383 var index = getMaxSteps();
384 runtime.debuggerStatus.eventCounter = 0;
385 debugModule.tracePath.clearTraces();
387 // draw new trace path
388 debugModule.tracePath.startRecording();
389 while(currentEventIndex < index) {
390 var nextEvent = debugModule.getRecordedEventAt(currentEventIndex);
391 runtime.debuggerStatus.eventCounter += 1;
392 runtime.notify(nextEvent.id, nextEvent.value, nextEvent.timestep);
393 debugModule.snapshotOnCheckpoint();
394 currentEventIndex += 1;
395 }
396 debugModule.tracePath.stopRecording();
398 stepTo(debuggerHistory.currentEventIndex);
399 if (!paused) {
400 debugModule.setContinue(debuggerHistory.currentEventIndex);
401 }
402 }
404 runtime.node.parentNode.appendChild(debugModule.tracePath.canvas);
406 var elmDebugger = {
407 restart: restartProgram,
408 pause: pauseProgram,
409 kontinue: continueProgram,
410 getMaxSteps: getMaxSteps,
411 stepTo: stepTo,
412 getPaused: debugModule.getPaused,
413 getHotSwapState: getHotSwapState,
414 dispose: dispose,
415 allNodes: debugModule.signalGraphNodes,
416 watchTracker: debugModule.watchTracker
417 };
419 return elmDebugger;
422function Point(x, y) {
423 this.x = x;
424 this.y = y;
426 this.translate = function(x, y) {
427 return new Point(this.x + x, this.y + y);
428 }
430 this.equals = function(p) {
431 return this.x == p.x && this.y == p.y;
432 }
435function tracePathInit(runtime, signalGraphMain) {
436 var List = Elm.List.make(runtime);
437 var Signal = Elm.Signal.make(runtime);
438 var tracePathNode = A2(Signal.lift, graphicsUpdate, signalGraphMain);
439 var tracePathCanvas = createCanvas();
440 var tracePositions = {};
441 var recordingTraces = true;
443 function findPositions(currentScene) {
444 var positions = {};
445 function processElement(elem, offset) {
446 if (elem.element.ctor == "Custom" && elem.element.type == "Collage")
447 {
448 List.map(F2(processForm)(offset))(elem.element.model.forms);
449 }
450 }
452 function processForm(offset, form) {
453 if (form.form.ctor == "FElement")
454 {
455 processElement(form.form._0, offset.translate(form.x, -form.y));
456 }
457 if (form.form.ctor == "FGroup")
458 {
459 var newOffset = offset.translate(form.x, -form.y);
460 List.map(F2(processForm)(newOffset))(form.form._1);
461 }
462 if (form.debugTracePathId)
463 {
464 positions[form.debugTracePathId] = new Point(form.x + offset.x, -form.y + offset.y);
465 }
466 }
468 processElement(currentScene, new Point(0, 0));
469 return positions;
470 }
472 function appendPositions(positions) {
473 for (var id in positions) {
474 var pos = positions[id];
475 if (tracePositions.hasOwnProperty(id)) {
476 tracePositions[id].push(pos);
477 }
478 else {
479 tracePositions[id] = [pos];
480 }
481 if (tracePositions[id].length < runtime.debuggerStatus.eventCounter) {
482 var padCount = runtime.debuggerStatus.eventCounter - tracePositions[id].length;
483 var lastTracePosition = tracePositions[id][tracePositions[id].length - 1];
484 for (var i = padCount; i--;) {
485 tracePositions[id].push(lastTracePosition)
486 }
487 }
488 assert(tracePositions[id].length === runtime.debuggerStatus.eventCounter,
489 "We don't have a 1-1 mapping of trace positions to events");
490 }
491 }
493 function graphicsUpdate(currentScene) {
494 if (!recordingTraces) {
495 return;
496 }
498 var ctx = tracePathCanvas.getContext('2d');
499 ctx.clearRect(0, 0, tracePathCanvas.width, tracePathCanvas.height);
501 ctx.save();
502 ctx.translate(ctx.canvas.width/2, ctx.canvas.height/2);
503 appendPositions(findPositions(currentScene));
504 for (var id in tracePositions)
505 {
506 ctx.beginPath();
507 var points = tracePositions[id];
508 for (var i=0; i < points.length; i++)
509 {
510 var p = points[i];
511 if (i == 0) {
512 ctx.moveTo(p.x, p.y);
513 }
514 else {
515 ctx.lineTo(p.x, p.y);
516 }
517 }
518 ctx.lineWidth = 1;
519 ctx.strokeStyle = "rgba(50, 50, 50, 0.4)";
520 ctx.stroke();
521 }
523 ctx.restore();
524 }
526 function clearTraces() {
527 tracePositions = {};
528 }
530 function stopRecording() {
531 recordingTraces = false;
532 }
534 function startRecording() {
535 recordingTraces = true;
536 }
538 function clearTracesAfter(position) {
539 var newTraces = {};
540 for (var id in tracePositions) {
541 newTraces[id] = tracePositions[id].slice(0,position);
542 }
543 tracePositions = newTraces;
544 }
546 return {
547 graphicsUpdate: graphicsUpdate,
548 canvas: tracePathCanvas,
549 clearTraces: clearTraces,
550 clearTracesAfter: clearTracesAfter,
551 stopRecording: stopRecording,
552 startRecording: startRecording
553 }
556function executeCallbacks(callbacks) {
557 callbacks.forEach(function(timer) {
558 if (!timer.executed) {
559 var func = timer.func;
560 timer.executed = true;
561 func();
562 }
563 });
566function createCanvas() {
567 var c = document.createElement('canvas');
568 c.width = window.innerWidth;
569 c.height = window.innerHeight;
570 c.style.position = "absolute";
571 c.style.top = "0";
572 c.style.left = "0";
573 c.style.pointerEvents = "none";
574 return c;
577function assert(bool, msg) {
578 if (!bool) {
579 throw "Assertion error: " + msg;
580 }
583function snapshotSignalGraph(signalGraphNodes) {
584 var nodeValues = [];
586 signalGraphNodes.forEach(function(node) {
587 nodeValues.push({ value: node.value, id: node.id });
588 });
590 return nodeValues;
591};
593function restoreSnapshot(signalGraphNodes, snapshot) {
594 assert(signalGraphNodes.length == snapshot.length,
595 "saved program state has wrong length");
596 for (var i=0; i < signalGraphNodes.length; i++) {
597 var node = signalGraphNodes[i];
598 var state = snapshot[i];
599 assert(node.id == state.id, "the nodes moved position");
601 node.value = state.value;
602 }
605function flattenSignalGraph(nodes) {
606 var nodesById = {};
608 function addAllToDict(node) {
609 nodesById[node.id] = node;
610 node.kids.forEach(addAllToDict);
611 }
612 nodes.forEach(addAllToDict);
614 var allNodes = Object.keys(nodesById).sort().map(function(key) {
615 return nodesById[key];
616 });
617 return allNodes;
618};
620}());