Skip to content

Commit 33d8a2d

Browse files
committed
Switched to ws4py from websocket-client
It's ~40% faster at processing trace file messages on a pi
1 parent 0bb36d4 commit 33d8a2d

26 files changed

+3859
-24
lines changed

Dockerfile

-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ RUN pip install \
3838
psutil \
3939
requests \
4040
ujson \
41-
websocket-client \
4241
xvfbwrapper
4342

4443
COPY wptagent.py /wptagent/wptagent.py

docs/install.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,13 @@ wptagent currently supports Windows and Linux hosts (possibly OSX but not tested
1010
* pypiwin32 (Windows only)
1111
* requests
1212
* ujson
13-
* websocket-client
1413
* xvfbwrapper (Linux only)
1514
* Imagemagick installed and available in the path
1615
* The legacy tools (convert, compare, etc) need to be installed which may be optional on Windows
1716
* ffmpeg installed and available in the path
1817
* Xvfb (Linux only)
1918
* Debian:
20-
* ```sudo apt-get install -y python2.7 python-pip imagemagick ffmpeg xvfb && sudo pip install dnspython monotonic pillow psutil requests ujson websocket-client xvfbwrapper```
19+
* ```sudo apt-get install -y python2.7 python-pip imagemagick ffmpeg xvfb && sudo pip install dnspython monotonic pillow psutil requests ujson xvfbwrapper```
2120
* Chrome Browser
2221
* Linux stable channel on Ubuntu/Debian:
2322
* ```wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -```

internal/devtools.py

+50-14
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,17 @@
66
import gzip
77
import logging
88
import os
9-
import re
9+
import Queue
1010
import subprocess
1111
import time
1212
import monotonic
1313
import ujson as json
14+
from ws4py.client.threadedclient import WebSocketClient
1415

1516
class DevTools(object):
1617
"""Interface into Chrome's remote dev tools protocol"""
1718
def __init__(self, job, task, use_devtools_video):
18-
self.url = "http://localhost:{0:d}/json".format(task['port'])
19+
self.url = "http://127.0.0.1:{0:d}/json".format(task['port'])
1920
self.websocket = None
2021
self.job = job
2122
self.task = task
@@ -84,11 +85,13 @@ def connect(self, timeout):
8485
# Close extra tabs
8586
requests.get(self.url + '/close/' + tabs[index]['id'])
8687
if websocket_url is not None:
87-
from websocket import create_connection
88-
self.websocket = create_connection(websocket_url)
89-
if self.websocket:
90-
self.websocket.settimeout(1)
88+
try:
89+
self.websocket = DevToolsClient(websocket_url)
90+
self.websocket.connect()
9191
ret = True
92+
except Exception:
93+
logging.critical("Connect to dev tools websocket Error: %s",
94+
err.__str__())
9295
else:
9396
time.sleep(0.5)
9497
else:
@@ -182,10 +185,9 @@ def collect_trace(self):
182185
logging.info('Collecting trace events')
183186
done = False
184187
last_message = monotonic.monotonic()
185-
self.websocket.settimeout(1)
186188
while not done and monotonic.monotonic() - last_message < 30:
187189
try:
188-
raw = self.websocket.recv()
190+
raw = self.websocket.get_message(1)
189191
if raw is not None and len(raw):
190192
msg = json.loads(raw)
191193
if 'method' in msg:
@@ -346,9 +348,8 @@ def flush_pending_messages(self):
346348
"""Clear out any pending websocket messages"""
347349
if self.websocket:
348350
try:
349-
self.websocket.settimeout(0)
350351
while True:
351-
raw = self.websocket.recv()
352+
raw = self.websocket.get_message(0)
352353
if raw is not None and len(raw):
353354
logging.debug(raw[:1000])
354355
msg = json.loads(raw)
@@ -369,11 +370,10 @@ def send_command(self, method, params, wait=False, timeout=30):
369370
logging.debug("Sending: %s", out)
370371
self.websocket.send(out)
371372
if wait:
372-
self.websocket.settimeout(1)
373373
end_time = monotonic.monotonic() + timeout
374374
while ret is None and monotonic.monotonic() < end_time:
375375
try:
376-
raw = self.websocket.recv()
376+
raw = self.websocket.get_message(1)
377377
if raw is not None and len(raw):
378378
logging.debug(raw[:1000])
379379
msg = json.loads(raw)
@@ -389,13 +389,12 @@ def send_command(self, method, params, wait=False, timeout=30):
389389
def wait_for_page_load(self):
390390
"""Wait for the page load and activity to finish"""
391391
if self.websocket:
392-
self.websocket.settimeout(1)
393392
start_time = monotonic.monotonic()
394393
end_time = start_time + self.task['time_limit']
395394
done = False
396395
while not done:
397396
try:
398-
raw = self.websocket.recv()
397+
raw = self.websocket.get_message(1)
399398
if raw is not None and len(raw):
400399
logging.debug(raw[:1000])
401400
msg = json.loads(raw)
@@ -671,3 +670,40 @@ def get_header_value(self, headers, name):
671670
value = headers[header_name]
672671
break
673672
return value
673+
674+
class DevToolsClient(WebSocketClient):
675+
"""DevTools Websocket client"""
676+
def __init__(self, url, protocols=None, extensions=None, heartbeat_freq=None,
677+
ssl_options=None, headers=None):
678+
WebSocketClient.__init__(self, url, protocols, extensions, heartbeat_freq,
679+
ssl_options, headers)
680+
self.connected = False
681+
self.messages = Queue.Queue()
682+
683+
def opened(self):
684+
"""Websocket interface - connection opened"""
685+
logging.debug("DevTools websocket connected")
686+
self.connected = True
687+
688+
def closed(self, code, reason=None):
689+
"""Websocket interface - connection closed"""
690+
logging.debug("DevTools websocket disconnected")
691+
self.connected = False
692+
693+
def received_message(self, raw):
694+
"""Websocket interface - message received"""
695+
if raw.is_text:
696+
message = raw.data.decode(raw.encoding) if raw.encoding is not None else raw.data
697+
self.messages.put(message)
698+
699+
def get_message(self, timeout):
700+
"""Wait for and return a message from the queue"""
701+
message = None
702+
try:
703+
if timeout is None or timeout <= 0:
704+
message = self.messages.get_nowait()
705+
else:
706+
message = self.messages.get(True, timeout)
707+
except Exception:
708+
pass
709+
return message

ubuntu_install.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/bin/bash
22
sudo apt-get install -y python-pip imagemagick ffmpeg xvfb
3-
sudo pip install dnspython monotonic pillow psutil requests ujson websocket-client xvfbwrapper
3+
sudo pip install dnspython monotonic pillow psutil requests ujson xvfbwrapper
44
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
55
sudo sh -c 'echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
66
sudo apt-get update

wptagent.py

-6
Original file line numberDiff line numberDiff line change
@@ -167,12 +167,6 @@ def startup(self):
167167
print "Missing ujson parser. Please run 'pip install ujson'"
168168
ret = False
169169

170-
try:
171-
import websocket as _
172-
except ImportError:
173-
print "Missing websocket module. Please run 'pip install websocket-client'"
174-
ret = False
175-
176170
try:
177171
subprocess.check_output(['python', '--version'])
178172
except Exception:

ws4py/LICENSE

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
NOTICE: The version of ws4py included with kaithem has been modified.
2+
3+
4+
Copyright (c) 2011-2015, Sylvain Hellegouarch
5+
All rights reserved.
6+
7+
Redistribution and use in source and binary forms, with or without
8+
modification, are permitted provided that the following conditions are met:
9+
10+
* Redistributions of source code must retain the above copyright notice,
11+
this list of conditions and the following disclaimer.
12+
* Redistributions in binary form must reproduce the above copyright
13+
notice, this list of conditions and the following disclaimer in the
14+
documentation and/or other materials provided with the distribution.
15+
* Neither the name of ws4py nor the names of its contributors may be used
16+
to endorse or promote products derived from this software without
17+
specific prior written permission.
18+
19+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22+
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
23+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28+
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29+
POSSIBILITY OF SUCH DAMAGE.

ws4py/__init__.py

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Redistribution and use in source and binary forms, with or without
4+
# modification, are permitted provided that the following conditions are
5+
# met:
6+
#
7+
# * Redistributions of source code must retain the above copyright
8+
# notice, this list of conditions and the following disclaimer.
9+
# * Redistributions in binary form must reproduce the above
10+
# copyright notice, this list of conditions and the following disclaimer
11+
# in the documentation and/or other materials provided with the
12+
# distribution.
13+
# * Neither the name of ws4py nor the names of its
14+
# contributors may be used to endorse or promote products derived from
15+
# this software without specific prior written permission.
16+
#
17+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20+
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21+
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23+
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24+
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25+
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28+
#
29+
import logging
30+
import logging.handlers as handlers
31+
32+
__author__ = "Sylvain Hellegouarch"
33+
__version__ = "0.4.2.dev0"
34+
__all__ = ['WS_KEY', 'WS_VERSION', 'configure_logger', 'format_addresses']
35+
36+
WS_KEY = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
37+
WS_VERSION = (8, 13)
38+
39+
def configure_logger(stdout=True, filepath=None, level=logging.INFO):
40+
logger = logging.getLogger('ws4py')
41+
logger.setLevel(level)
42+
logfmt = logging.Formatter("[%(asctime)s] %(levelname)s %(message)s")
43+
44+
if filepath:
45+
h = handlers.RotatingFileHandler(filepath, maxBytes=10485760, backupCount=3)
46+
h.setLevel(level)
47+
h.setFormatter(logfmt)
48+
logger.addHandler(h)
49+
50+
if stdout:
51+
import sys
52+
h = logging.StreamHandler(sys.stdout)
53+
h.setLevel(level)
54+
h.setFormatter(logfmt)
55+
logger.addHandler(h)
56+
57+
return logger
58+
59+
def format_addresses(ws):
60+
me = ws.local_address
61+
peer = ws.peer_address
62+
if isinstance(me, tuple) and isinstance(peer, tuple):
63+
me_ip, me_port = ws.local_address
64+
peer_ip, peer_port = ws.peer_address
65+
return "[Local => %s:%d | Remote => %s:%d]" % (me_ip, me_port, peer_ip, peer_port)
66+
67+
return "[Bound to '%s']" % me

ws4py/async_websocket.py

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# -*- coding: utf-8 -*-
2+
__doc__ = """
3+
WebSocket implementation that relies on two new Python
4+
features:
5+
6+
* asyncio to provide the high-level interface above transports
7+
* yield from to delegate to the reading stream whenever more
8+
bytes are required
9+
10+
You can use these implementations in that context
11+
and benefit from those features whilst using ws4py.
12+
13+
Strictly speaking this module probably doesn't have to
14+
be called async_websocket but it feels this will be its typical
15+
usage and is probably more readable than
16+
delegated_generator_websocket_on_top_of_asyncio.py
17+
"""
18+
import asyncio
19+
import types
20+
21+
from ws4py.websocket import WebSocket as _WebSocket
22+
from ws4py.messaging import Message
23+
24+
__all__ = ['WebSocket', 'EchoWebSocket']
25+
26+
class WebSocket(_WebSocket):
27+
def __init__(self, proto):
28+
"""
29+
A :pep:`3156` ready websocket handler that works
30+
well in a coroutine-aware loop such as the one provided
31+
by the asyncio module.
32+
33+
The provided `proto` instance is a
34+
:class:`asyncio.Protocol` subclass instance that will
35+
be used internally to read and write from the
36+
underlying transport.
37+
38+
Because the base :class:`ws4py.websocket.WebSocket`
39+
class is still coupled a bit to the socket interface,
40+
we have to override a little more than necessary
41+
to play nice with the :pep:`3156` interface. Hopefully,
42+
some day this will be cleaned out.
43+
"""
44+
_WebSocket.__init__(self, None)
45+
self.started = False
46+
self.proto = proto
47+
48+
@property
49+
def local_address(self):
50+
"""
51+
Local endpoint address as a tuple
52+
"""
53+
if not self._local_address:
54+
self._local_address = self.proto.reader.transport.get_extra_info('sockname')
55+
if len(self._local_address) == 4:
56+
self._local_address = self._local_address[:2]
57+
return self._local_address
58+
59+
@property
60+
def peer_address(self):
61+
"""
62+
Peer endpoint address as a tuple
63+
"""
64+
if not self._peer_address:
65+
self._peer_address = self.proto.reader.transport.get_extra_info('peername')
66+
if len(self._peer_address) == 4:
67+
self._peer_address = self._peer_address[:2]
68+
return self._peer_address
69+
70+
def once(self):
71+
"""
72+
The base class directly is used in conjunction with
73+
the :class:`ws4py.manager.WebSocketManager` which is
74+
not actually used with the asyncio implementation
75+
of ws4py. So let's make it clear it shan't be used.
76+
"""
77+
raise NotImplemented()
78+
79+
def close_connection(self):
80+
"""
81+
Close the underlying transport
82+
"""
83+
@asyncio.coroutine
84+
def closeit():
85+
yield from self.proto.writer.drain()
86+
self.proto.writer.close()
87+
asyncio.async(closeit())
88+
89+
def _write(self, data):
90+
"""
91+
Write to the underlying transport
92+
"""
93+
@asyncio.coroutine
94+
def sendit(data):
95+
self.proto.writer.write(data)
96+
yield from self.proto.writer.drain()
97+
asyncio.async(sendit(data))
98+
99+
@asyncio.coroutine
100+
def run(self):
101+
"""
102+
Coroutine that runs until the websocket
103+
exchange is terminated. It also calls the
104+
`opened()` method to indicate the exchange
105+
has started.
106+
"""
107+
self.started = True
108+
try:
109+
self.opened()
110+
reader = self.proto.reader
111+
while True:
112+
data = yield from reader.read(self.reading_buffer_size)
113+
if not self.process(data):
114+
return False
115+
finally:
116+
self.terminate()
117+
118+
return True
119+
120+
class EchoWebSocket(WebSocket):
121+
def received_message(self, message):
122+
"""
123+
Automatically sends back the provided ``message`` to
124+
its originating endpoint.
125+
"""
126+
self.send(message.data, message.is_binary)

0 commit comments

Comments
 (0)