aboutsummaryrefslogtreecommitdiff
path: root/tools/reproduceriter.py
blob: a1912396ea936431ddb944d698ee40dcc0a01f79 (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
#!/usr/bin/env python

'''
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(__file__), 'reproduceriter_shell.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!'