aboutsummaryrefslogtreecommitdiff
path: root/tools/reproduceriter.py
blob: df5b7df7aeaea2d1ccc968d3d648dab368faa137 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
'''

* This is a work in progress *

Reproducer Rewriter
===================

Processes a project and rewrites it so as to generate deterministic,
reproducible automatic results. For example, you can run this on a
game, and then when the game is run it will record user input and
sources of nondeterminism like Math.random(). You can then run
that recording as a benchmark or as a demo, it should give nearly
identical results every time it is run to the data that was
recorded.

Usage:

1. Run this script as

    reproduceriter.py IN_DIR OUT_DIR FIRST_JS START_COMMAND

   IN_DIR should be the project directory, and OUT_DIR will be
   created with the instrumented code (OUT_DIR should not
   exist). FIRST_JS should be a path (relative to IN_DIR) to
   the first JavaScript file loaded by the project (this tool
   will add code to that). 

2. Run the instrumented project in OUR_DIR and interact with
   the program. When you are done recording, open the web
   console and run

    Recorder.finish();

   This will write out the recorded data into the current tab.
   Save it as

    repro.data

   in OUT_DIR.

3. To re-play the recorded data, run the instrumented build
   with

    &reproduce=repro.data

Notes:

 * When we start to replay events, the assumption is that
   there is nothing asynchronous that affects execution. So
   asynchronous loading of files should have already
   completed.

   TODO: start running recorded events with some trigger, for example the fullscreen button in BananaBread
'''

import os, sys, shutil

assert len(sys.argv) == 4, 'Usage: reproduceriter.py IN_DIR OUT_DIR'

# Process input args

in_dir = sys.argv[1]
out_dir = sys.argv[2]
first_js = sys.argv[3]

assert not os.path.exists(out_dir), 'OUT_DIR must not exist'
assert os.path.exists(os.path.join(in_dir, first_js))

# Copy project

shutil.copytree(in_dir, out_dir)

# Add our boilerplate

open(os.path.join(out_dir, first_js), 'w').write('''
var Recorder = (function() {
  var recorder;
  var init = '&reproduce=';
  var initLocation = window.location.search.indexOf(init);
  var replaying = initLocation >= 0
  if (replaying) {
    // load recording
    var dataPath = window.location.search.substring(initLocation);
    var baseURL = window.location.toString().replace('://', 'cheez999').split('?')[0].split('/')[0].replace('cheez999', '://');
    if (baseURL[baseURL.length-1] != '/') baseURL += '/';
    var request = new XMLHttpRequest();
    request.open('GET', baseURL + dataPath, false);
    request.send();
    recorder = JSON.parse(request.responseText);
    // prepare to replay
    // Math.random
    var warned = false;
    Math.random = function() {
      if (recorder.randoms.length > 0) {
        return recorder.randoms.shift();
      } else {
        if (!warned) {
          console.log('warning: consuming too many values!')
          warned = true;
        }
        return Math.random();
      }
    };
    // Date.now, performance.now
    var warned = false;
    Date.now = function() {
      if (recorder.dnows.length > 0) {
        return recorder.dnows.shift();
      } else {
        if (!warned) {
          console.log('warning: consuming too many values!')
          warned = true;
        }
        return Date.now();
      }
    };
    var warned = false;
    performance.now = function() {
      if (recorder.pnows.length > 0) {
        return recorder.pnows.shift();
      } else {
        if (!warned) {
          console.log('warning: consuming too many values!')
          warned = true;
        }
        return performance.now();
      }
    };
  } else {
    // prepare to record
    recorder = {};
    // Math.random
    recorder.randoms = [];
    var random = Math.random()
    Math.random = function() {
      var ret = random();
      recorder.randoms.push(ret);
      return ret;
    };
    // Date.now, performance.now
    recorder.dnows = [];
    var dnow = Date.now();
    Date.now = function() {
      var ret = dnow();
      recorder.dnows.push(ret);
      return ret;
    };
    recorder.pnows = [];
    var pnow = performance.now();
    performance.now = function() {
      var ret = pnow();
      recorder.pnows.push(ret);
      return ret;
    };
    // finish
    recorder.finish = function() {
      document.write(JSON.stringify(recorder));
      throw 'all done, remember to save!';
    };
  }
  return recorder;
})();
''' + open(os.path.join(in_dir, first_js)).read()
)