on 021af44 help sign in
elk/runtime/Debugger.js
543 lines 15 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.debuggerAttach = function(module, hotSwapState /* =undefined */) {
13 return {
14 make: function(runtime) {
15 var wrappedModule = debugModule(module, runtime);
16 Elm.Debugger = debuggerInit(wrappedModule, runtime, hotSwapState);
17 dispatchElmDebuggerInit();
18 return wrappedModule.debuggedModule;
19 }
20 };
21};
23var EVENTS_PER_SAVE = 100;
25function dispatchElmDebuggerInit() {
26 if (parent.window) {
27 var dispatch = function() {
28 parent.window.postMessage("elmDebuggerInit", window.location.origin);
29 };
30 if (parent.window.document.readyState === "complete") {
31 dispatch();
32 }
33 else {
34 parent.window.addEventListener("load", dispatch);
35 }
36 }
39function debugModule(module, runtime) {
40 var programPaused = false;
41 var recordedEvents = [];
42 var asyncCallbacks = [];
43 var snapshots = [];
44 var watchTracker = Elm.Native.Debug.make(runtime).watchTracker;
45 var pauseTime = 0;
46 var eventsUntilSnapshot = EVENTS_PER_SAVE;
48 // runtime is the prototype of wrappedRuntime
49 // so we can access all runtime properties too
50 var wrappedRuntime = Object.create(runtime);
51 wrappedRuntime.notify = wrapNotify;
52 wrappedRuntime.runDelayed = wrapRunDelayed;
54 // make a copy of the wrappedRuntime
55 var assignedPropTracker = Object.create(wrappedRuntime);
56 var debuggedModule = module.make(assignedPropTracker);
58 // make sure the signal graph is actually a signal & extract the visual model
59 if ( !('recv' in debuggedModule.main) ) {
60 debuggedModule.main = Elm.Signal.make(runtime).constant(debuggedModule.main);
61 }
63 // The main module stores imported modules onto the runtime.
64 // To ensure only one instance of each module is created,
65 // we assign them back on the original runtime object.
66 Object.keys(assignedPropTracker).forEach(function(key) {
67 runtime[key] = assignedPropTracker[key];
68 });
70 var signalGraphNodes = flattenSignalGraph(wrappedRuntime.inputs);
71 var tracePath = tracePathInit(runtime, debuggedModule.main);
73 snapshots.push(snapshotSignalGraph(signalGraphNodes));
75 function wrapNotify(id, v) {
76 var timestep = runtime.timer.now();
78 if (programPaused) {
79 // ignore async events generated while playing back
80 // or user events while program is paused
81 return false;
82 }
83 else {
84 var changed = runtime.notify(id, v, timestep);
85 recordEvent(id, v, timestep);
86 if (eventsUntilSnapshot === 1) {
87 saveSnapshot();
88 eventsUntilSnapshot = EVENTS_PER_SAVE;
89 } else {
90 eventsUntilSnapshot -= 1;
91 }
92 if (parent.window) {
93 parent.window.postMessage("elmNotify", window.location.origin);
94 }
95 return changed;
96 }
97 };
99 function wrapRunDelayed(func, delayMs) {
100 if (programPaused) {
101 // Don't push timers and such to the callback stack while we're paused.
102 // It causes too many callbacks to be fired during unpausing.
103 return 0;
104 }
105 var cbObj = { func:func, delayMs:delayMs, timerId:0, executed:false };
106 var timerId = setTimeout(function() {
107 cbObj.executed = true;
108 func();
109 }, delayMs);
110 cbObj.timerId = timerId;
111 asyncCallbacks.push(cbObj);
112 return timerId;
113 }
115 function recordEvent(id, v, timestep) {
116 watchTracker.pushFrame();
117 recordedEvents.push({ id:id, value:v, timestep:timestep });
118 }
120 function clearAsyncCallbacks() {
121 asyncCallbacks.forEach(function(timer) {
122 if (!timer.executed) {
123 clearTimeout(timer.timerId);
124 }
125 });
126 }
128 function clearRecordedEvents() {
129 recordedEvents = [];
130 }
132 function getRecordedEventsLength() {
133 return recordedEvents.length;
134 }
136 function getRecordedEventAt(i) {
137 return recordedEvents[i];
138 }
140 function copyRecordedEvents() {
141 return recordedEvents.slice();
142 }
144 function loadRecordedEvents(events) {
145 recordedEvents = events.slice();
146 }
148 function clearSnapshots() {
149 snapshots = [snapshotSignalGraph(signalGraphNodes)];
150 }
152 function getSnapshotAt(i) {
153 var snapshotEvent = Math.floor(i / EVENTS_PER_SAVE);
154 assert(snapshotEvent < snapshots.length && snapshotEvent >= 0, "Out of bounds index: " + snapshotEvent);
155 return snapshots[snapshotEvent];
156 }
158 function saveSnapshot() {
159 snapshots.push(snapshotSignalGraph(signalGraphNodes));
160 }
162 function setPaused() {
163 programPaused = true;
164 clearAsyncCallbacks();
165 tracePath.stopRecording();
166 pauseTime = Date.now();
167 }
169 function setContinue(position) {
170 var timerDelay = Date.now() - pauseTime;
171 runtime.timer.addDelay(timerDelay);
172 programPaused = false;
173 if (position > 0) {
174 // If we're not unpausing at the head, then we need to dump the
175 // events that are ahead of where we're continuing.
176 var lastSnapshotPosition = Math.ceil(position / EVENTS_PER_SAVE);
177 eventsUntilSnapshot = EVENTS_PER_SAVE - (position % EVENTS_PER_SAVE);
178 snapshots = snapshots.slice(0, lastSnapshotPosition);
179 recordedEvents = recordedEvents.slice(0, position);
180 executeCallbacks(asyncCallbacks, false);
181 }
182 tracePath.startRecording();
183 }
185 function getPaused() {
186 return programPaused;
187 }
189 return {
190 debuggedModule: debuggedModule,
191 signalGraphNodes: signalGraphNodes,
192 initialSnapshot: snapshotSignalGraph(signalGraphNodes),
193 initialAsyncCallbacks: asyncCallbacks.slice(),
194 // API functions
195 clearAsyncCallbacks: clearAsyncCallbacks,
196 clearRecordedEvents: clearRecordedEvents,
197 getRecordedEventsLength: getRecordedEventsLength,
198 getRecordedEventAt: getRecordedEventAt,
199 copyRecordedEvents: copyRecordedEvents,
200 loadRecordedEvents: loadRecordedEvents,
201 clearSnapshots: clearSnapshots,
202 getSnapshotAt: getSnapshotAt,
203 saveSnapshot: saveSnapshot,
204 getPaused: getPaused,
205 setPaused: setPaused,
206 setContinue: setContinue,
207 tracePath: tracePath,
208 watchTracker: watchTracker
209 };
212function debuggerInit(debugModule, runtime, hotSwapState /* =undefined */) {
213 var currentEventIndex = 0;
215 function resetProgram(position) {
216 var closestSnapshot = debugModule.getSnapshotAt(position);
217 debugModule.clearAsyncCallbacks();
218 restoreSnapshot(debugModule.signalGraphNodes, closestSnapshot);
219 // clear traces here.
220 redrawGraphics();
221 }
223 function restartProgram() {
224 resetProgram(0);
225 debugModule.watchTracker.clear();
226 debugModule.tracePath.clearTraces();
227 debugModule.clearRecordedEvents();
228 debugModule.clearSnapshots();
229 debugModule.setContinue(0);
230 executeCallbacks(debugModule.initialAsyncCallbacks, true);
231 }
233 function pauseProgram() {
234 debugModule.setPaused();
235 currentEventIndex = debugModule.getRecordedEventsLength();
236 }
238 function continueProgram() {
239 if (debugModule.getPaused())
240 {
241 if (currentEventIndex === 0) {
242 restartProgram();
243 return;
244 }
245 var closestSnapshotIndex = Math.floor(currentEventIndex / EVENTS_PER_SAVE) * EVENTS_PER_SAVE;
246 resetProgram(currentEventIndex);
247 var continueIndex = currentEventIndex;
248 currentEventIndex = closestSnapshotIndex;
249 stepTo(continueIndex);
250 debugModule.setContinue(currentEventIndex);
251 }
252 }
254 function stepTo(index) {
255 if (!debugModule.getPaused()) {
256 debugModule.setPaused();
257 resetProgram();
258 }
260 if (index < 0 || index > getMaxSteps()) {
261 throw "Index out of bounds: " + index;
262 }
264 if (index < currentEventIndex) {
265 var closestSnapshotIndex = Math.floor(index / EVENTS_PER_SAVE) * EVENTS_PER_SAVE;
266 resetProgram(index);
267 currentEventIndex = closestSnapshotIndex;
268 }
270 while (currentEventIndex < index) {
271 var nextEvent = debugModule.getRecordedEventAt(currentEventIndex);
272 runtime.notify(nextEvent.id, nextEvent.value, nextEvent.timestep);
274 currentEventIndex += 1;
275 }
276 }
278 function getMaxSteps() {
279 return debugModule.getRecordedEventsLength();
280 }
282 function redrawGraphics() {
283 var main = debugModule.debuggedModule.main
284 for (var i = main.kids.length ; i-- ; ) {
285 main.kids[i].recv(runtime.timer.now(), true, main.id);
286 }
287 }
289 function getHotSwapState() {
290 var continueIndex = currentEventIndex;
291 if (!debugModule.getPaused()) {
292 continueIndex = getMaxSteps();
293 }
294 return {
295 recordedEvents: debugModule.copyRecordedEvents(),
296 currentEventIndex: continueIndex
297 };
298 }
300 function dispose() {
301 var parentNode = runtime.node.parentNode;
302 parentNode.removeChild(debugModule.tracePath.canvas);
303 parentNode.removeChild(runtime.node);
304 }
306 if (hotSwapState) {
307 // The problem is that we want to previous paused state. But
308 // by the time JS reaches here, the old code has been swapped out
309 // and the new modules are being generated. So we can ask the
310 // debugging console what it thinks the pause state is and go
311 // from there.
312 var paused = top.debug.paused;
313 debugModule.setPaused();
314 debugModule.loadRecordedEvents(hotSwapState.recordedEvents);
315 var index = getMaxSteps();
316 var eventsUntilSnapshot = EVENTS_PER_SAVE;
318 // draw new trace path
319 debugModule.tracePath.startRecording();
320 while(currentEventIndex < index) {
321 var nextEvent = debugModule.getRecordedEventAt(currentEventIndex);
322 runtime.notify(nextEvent.id, nextEvent.value, nextEvent.timestep);
323 if (eventsUntilSnapshot === 1) {
324 debugModule.saveSnapshot();
325 eventsUntilSnapshot = EVENTS_PER_SAVE;
326 } else {
327 eventsUntilSnapshot -= 1;
328 }
329 currentEventIndex += 1;
330 }
331 debugModule.tracePath.stopRecording();
333 stepTo(hotSwapState.currentEventIndex);
334 if (!paused) {
335 debugModule.setContinue(hotSwapState.currentEventIndex);
336 }
337 }
339 runtime.node.parentNode.appendChild(debugModule.tracePath.canvas);
341 var elmDebugger = {
342 restart: restartProgram,
343 pause: pauseProgram,
344 kontinue: continueProgram,
345 getMaxSteps: getMaxSteps,
346 stepTo: stepTo,
347 getPaused: debugModule.getPaused,
348 getHotSwapState: getHotSwapState,
349 dispose: dispose,
350 allNodes: debugModule.signalGraphNodes,
351 watchTracker: debugModule.watchTracker
352 };
354 return elmDebugger;
357function Point(x, y) {
358 this.x = x;
359 this.y = y;
361 this.translate = function(x, y) {
362 return new Point(this.x + x, this.y + y);
363 }
365 this.equals = function(p) {
366 return this.x == p.x && this.y == p.y;
367 }
370function tracePathInit(runtime, signalGraphMain) {
371 var List = Elm.List.make(runtime);
372 var Signal = Elm.Signal.make(runtime);
373 var tracePathNode = A2(Signal.lift, graphicsUpdate, signalGraphMain);
374 var tracePathCanvas = createCanvas();
375 var tracePositions = {};
376 var recordingTraces = true;
378 function findPositions(currentScene) {
379 var positions = {};
380 function processElement(elem, offset) {
381 if (elem.element.ctor == "Custom" && elem.element.type == "Collage")
382 {
383 List.map(F2(processForm)(offset))(elem.element.model.forms);
384 }
385 if (elem.props.debugTracePathId)
386 {
387 positions[elem.props.debugTracePathId] = new Point(offset.x, offset.y);
388 }
389 }
391 function processForm(offset, form) {
392 if (form.form.ctor == "FElement")
393 {
394 processElement(form.form._0, offset.translate(form.x, -form.y));
395 }
396 }
398 processElement(currentScene, new Point(0, 0));
399 return positions;
400 }
402 function appendPositions(positions) {
403 for (var id in positions) {
404 var pos = positions[id];
405 if (tracePositions.hasOwnProperty(id)) {
406 var points = tracePositions[id];
407 if (!pos.equals(points[points.length-1])) {
408 points.push(pos);
409 }
410 }
411 else {
412 tracePositions[id] = [pos];
413 }
414 }
415 }
417 function graphicsUpdate(currentScene) {
418 if (!recordingTraces) {
419 return;
420 }
422 var ctx = tracePathCanvas.getContext('2d');
423 ctx.clearRect(0, 0, tracePathCanvas.width, tracePathCanvas.height);
425 ctx.save();
426 ctx.translate(ctx.canvas.width/2, ctx.canvas.height/2);
427 appendPositions(findPositions(currentScene));
428 for (var id in tracePositions)
429 {
430 ctx.beginPath();
431 var points = tracePositions[id];
432 for (var i=0; i < points.length; i++)
433 {
434 var p = points[i];
435 if (i == 0) {
436 ctx.moveTo(p.x, p.y);
437 }
438 else {
439 ctx.lineTo(p.x, p.y);
440 }
441 }
442 ctx.lineWidth = 1;
443 ctx.strokeStyle = "rgba(50, 50, 50, 0.4)";
444 ctx.stroke();
446 for (var i=0; i < points.length; i++)
447 {
448 var p = points[i];
449 ctx.beginPath();
450 ctx.arc(p.x, p.y, 2, 0, Math.PI * 2);
451 ctx.fillStyle = "rgba(50, 50, 50, 0.4)";
452 ctx.fill();
453 }
454 }
456 ctx.restore();
457 }
459 function clearTraces() {
460 tracePositions = {};
461 }
463 function stopRecording() {
464 recordingTraces = false;
465 }
467 function startRecording() {
468 recordingTraces = true;
469 }
471 return {
472 graphicsUpdate: graphicsUpdate,
473 canvas: tracePathCanvas,
474 clearTraces: clearTraces,
475 stopRecording: stopRecording,
476 startRecording: startRecording
477 }
480function executeCallbacks(callbacks, reexecute) {
481 callbacks.forEach(function(timer) {
482 if (reexecute || !timer.executed) {
483 var func = timer.func;
484 timer.executed = true;
485 func();
486 }
487 });
490function createCanvas() {
491 var c = document.createElement('canvas');
492 c.width = window.innerWidth;
493 c.height = window.innerHeight;
494 c.style.position = "absolute";
495 c.style.top = "0";
496 c.style.left = "0";
497 c.style.pointerEvents = "none";
498 return c;
501function assert(bool, msg) {
502 if (!bool) {
503 throw "Assertion error: " + msg;
504 }
507function snapshotSignalGraph(signalGraphNodes) {
508 var nodeValues = [];
510 signalGraphNodes.forEach(function(node) {
511 nodeValues.push({ value: node.value, id: node.id });
512 });
514 return nodeValues;
515};
517function restoreSnapshot(signalGraphNodes, snapshot) {
518 assert(signalGraphNodes.length == snapshot.length, "saved program state has wrong length");
519 for (var i=0; i < signalGraphNodes.length; i++) {
520 var node = signalGraphNodes[i];
521 var state = snapshot[i];
522 assert(node.id == state.id, "the nodes moved position");
524 node.value = state.value;
525 }
528function flattenSignalGraph(nodes) {
529 var nodesById = {};
531 function addAllToDict(node) {
532 nodesById[node.id] = node;
533 node.kids.forEach(addAllToDict);
534 }
535 nodes.forEach(addAllToDict);
537 var allNodes = Object.keys(nodesById).sort().map(function(key) {
538 return nodesById[key];
539 });
540 return allNodes;
541};
543}());