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
|
#!/usr/bin/env python2
'''
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 [WINDOW_LOCATION] [ON_IDLE]
IN_DIR should be the project directory, and OUT_DIR will be
created with the instrumented code (OUT_DIR will be overwritten
if it exists). 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). The last two parameters, WINDOW_LOCATION
and ON_IDLE, are relevant for shell builds. If WINDOW_LOCATION is
specified, we will make a build that runs in the shell and not in
a browser. WINDOW_LOCATION is the fake window.location we set in the
fake DOM, and ON_IDLE is code that runs when the fake main browser
event loop runs out of actions. (Note that only a browser build can
do recording, shell builds just replay.)
You will need to call
Recorder.start();
at the right time to start the relevant event loop. For
example, if your application is a game that starts receiving
events when in fullscreen, add something like
if (typeof Recorder != 'undefined') Recorder.start();
in the button that launches fullscreen. start() will start
either recording when in record mode, or replaying when
in replay mode, so you need this in both modes.
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
Note that as mentioned above you need to call
Recorder.start();
when the recorded event loop should start to replay.
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
Examples
* BananaBread: Unpack into a directory called bb, then one
directory up, run
emscripten/tools/reproduceriter.py bb bench js/game-setup.js game.html?low,low,reproduce=repro.data "function(){ print('triggering click'); document.querySelector('.fullscreen-button.low-res').callEventListeners('click'); window.onIdle = null; }"
for a shell build, or
emscripten/tools/reproduceriter.py bb bench js/game-setup.js
for a browser build. Since only a browser build can do recording, you would normally
make a browser build, record a trace, then make a shell build and copy the trace
there so you can run it.
The last parameter specifies what to do when the event loop is idle: We fire an event and then set onIdle (which was this function) to null, so this is a one-time occurence.
Notes
* Replay can depend on browser state. One case is if you are replaying a fullscreen
game with pointer lock, then you will need to manually allow pointer lock if it
isn't already on for the machine. If you do it too early or too late, the replay
can be different, since mousemove events mean different things depending on
whether the pointer is locked or not.
'''
import os, sys, shutil, re
assert len(sys.argv) >= 4, 'Usage: reproduceriter.py IN_DIR OUT_DIR FIRST_JS [WINDOW_LOCATION]'
# Process input args
in_dir = sys.argv[1]
out_dir = sys.argv[2]
first_js = sys.argv[3]
window_location = sys.argv[4] if len(sys.argv) >= 5 else ''
on_idle = sys.argv[5] if len(sys.argv) >= 6 else ''
shell = not not window_location
dirs_to_drop = 0 if not os.path.dirname(first_js) else len(os.path.dirname(first_js).split('/'))
if os.path.exists(out_dir):
shutil.rmtree(out_dir)
assert os.path.exists(os.path.join(in_dir, first_js))
# Copy project
print 'copying tree...'
shutil.copytree(in_dir, out_dir)
# Add customizations in all JS files
print 'add customizations...'
for parent, dirs, files in os.walk(out_dir):
for filename in files:
if filename.endswith('.js'):
fullname = os.path.join(parent, filename)
print ' ', fullname
js = open(fullname).read()
js = re.sub('document\.on(\w+) ?= ?([\w.$]+)', lambda m: 'Recorder.onEvent("' + m.group(1) + '", ' + m.group(2) + ')', js)
js = re.sub('''([\w.'"\[\]]+)\.addEventListener\(([\w,. $]+)\)''', lambda m: 'Recorder.addListener(' + m.group(1) + ', ' + m.group(2) + ')', js)
open(fullname, 'w').write(js)
# Add our boilerplate
print 'add boilerplate...'
open(os.path.join(out_dir, first_js), 'w').write(
(open(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'src', 'headless.js')).read() % (
window_location, window_location.split('?')[-1], on_idle or 'null', dirs_to_drop
) if shell else '') +
open(os.path.join(os.path.dirname(__file__), 'reproduceriter.js')).read() +
open(os.path.join(in_dir, first_js)).read() + ('\nwindow.runEventLoop();\n' if shell else '')
)
print 'done!'
|