-
Notifications
You must be signed in to change notification settings - Fork 52
/
Copy pathperformance.py
261 lines (208 loc) · 10.6 KB
/
performance.py
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
import time
from typing import List, Union
from pydantic import BaseModel, Field
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.support.wait import WebDriverWait
from pylenium.log import logger as log
def stopwatch(func):
"""Stopwatch Decorator.
Use this decorator on any function to measure how long it took
for the function to complete. This is in seconds, but may have fractions of a second
if the system clock provides more precision.
Notes:
This is _logged_, not printed to the Terminal
Examples:
```
# 1. How long does it take to add an item to the cart?
@stopwatch
def add_item_to_cart(py):
py.get('#add-item').click()
py.get('#added-notification').should().be_visible()
# 2. How long does it take to edit an item's available stock via the API and see it change in the UI?
@stopwatch
def update_available_stock(py, item, quantity):
payload = {'item': item, 'qty': quantity}
api.items.update(payload)
py.get(f'#available-stock-{item}').should().have_text(quantity)
```
"""
def wrapper(*args, **kwargs):
start_time = time.time()
func(*args, **kwargs)
stop_time = time.time()
func_name = func.__name__
log.debug("STOPWATCH - %s took %s seconds", func_name, stop_time - start_time)
return wrapper
class Performance:
"""Pylenium's Performance API."""
def __init__(self, webdriver):
self._webdriver = webdriver
def _wait(self, timeout=10):
return WebDriverWait(self._webdriver, timeout=timeout)
def get(self):
"""The main method used to generate a WebPerformance object from the current web page.
Notes:
Calling this method too soon may yield NoneTypes because the browser hasn't generated them yet.
Examples:
```
# Store the entire WebPerformance object and log it
perf = py.performance.get()
log.info(perf.dict())
# Get a single data point from WebPerformance
tti = py.performance.get().time_to_interactive()
```
"""
return WebPerformance(
time_origin=self.get_time_origin(),
navigation_timing=self.get_navigation_timing(),
paint_timing=self.get_paint_timing(),
resources=self.get_resources(),
)
def get_time_origin(self) -> float:
"""Returns the timeOrigin precision value.
This is the high resolution timestamp of the start time of the performance measurement.
"""
js = "return window.performance.timeOrigin;"
time_origin = self._wait().until(lambda driver: driver.execute_script(js), "Time Origin not generated yet")
return time_origin
def get_navigation_timing(self):
"""Return the PerformanceNavigationTiming object as a Python object."""
js = 'return window.performance.getEntriesByType("navigation")[0];'
navigation = self._wait().until(lambda driver: driver.execute_script(js), "NavigationTiming not generated yet")
return NavigationTiming(**navigation)
def get_paint_timing(self):
"""Return the PerformancePaintTiming object as a Python object."""
js = 'return window.performance.getEntriesByName("first-contentful-paint")[0];'
paint = self._wait().until(lambda driver: driver.execute_script(js), "PaintTiming not generated yet")
return PaintTiming(**paint)
def get_resources(self):
"""Return a list of PerformanceResourceTiming objects as Python objects."""
js = 'return window.performance.getEntriesByType("resource");'
try:
resources = self._wait().until(
lambda driver: driver.execute_script(js), message="Resources not generated yet or there are none"
)
return [ResourceTiming(**resource) for resource in resources]
except TimeoutException:
return None # because there were no Resources captured for the current web page
class NavigationTiming(BaseModel):
"""The PerformanceNavigationTiming Representation.
Metrics regarding the browser's document navigation events
References:
https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming
"""
connect_end: float = Field(alias="connectEnd")
connect_start: float = Field(alias="connectStart")
decoded_body_size: Union[int, float] = Field(alias="decodedBodySize")
dom_complete: float = Field(alias="domComplete")
dom_content_loaded_event_end: float = Field(alias="domContentLoadedEventEnd")
dom_content_loaded_event_start: float = Field(alias="domContentLoadedEventStart")
time_to_interactive: float = Field(alias="domInteractive")
domain_lookup_end: float = Field(alias="domainLookupEnd")
domain_lookup_start: float = Field(alias="domainLookupStart")
duration: float
encoded_body_size: Union[int, float] = Field(alias="encodedBodySize")
entry_type: str = Field(alias="entryType")
fetch_start: float = Field(alias="fetchStart")
initiator_type: str = Field(alias="initiatorType")
load_event_end: float = Field(alias="loadEventEnd")
load_event_start: float = Field(alias="loadEventStart")
name: str
next_hop_protocol: str = Field(alias="nextHopProtocol")
redirect_count: int = Field(alias="redirectCount")
redirect_end: int = Field(alias="redirectEnd")
redirect_start: int = Field(alias="redirectStart")
request_start: float = Field(alias="requestStart")
response_end: float = Field(alias="responseEnd")
response_start: float = Field(alias="responseStart")
secure_connection_start: float = Field(alias="secureConnectionStart")
server_timing: List = Field(alias="serverTiming")
start_time: int = Field(alias="startTime")
transfer_size: Union[int, float] = Field(alias="transferSize")
type: str
unload_event_end: int = Field(alias="unloadEventEnd")
unload_event_start: int = Field(alias="unloadEventStart")
worker_start: Union[int, float] = Field(alias="workerStart")
class PaintTiming(BaseModel):
"""The PerformancePaintTiming Representation.
Provides timing information about "paint" (also called "render") operations during web page construction.
References:
https://developer.mozilla.org/en-US/docs/Web/API/PerformancePaintTiming
"""
duration: float
entry_type: str = Field(alias="entryType", default="paint")
name: str = Field(default="first-contentful-paint")
start_time: float = Field(alias="startTime")
class ResourceTiming(BaseModel):
"""The PerformanceResourceTiming Representation.
Detailed network timing data regarding the loading of an application's resources.
An application can use the timing metrics to determine, for example, the length of time it takes
to fetch a specific resource, such as an XMLHttpRequest, <SVG>, image, or script.
References:
https://developer.mozilla.org/en-US/docs/web/api/performanceresourcetiming
"""
connect_end: float = Field(alias="connectEnd")
connect_start: float = Field(alias="connectStart")
decoded_body_size: int = Field(alias="decodedBodySize")
domain_lookup_end: float = Field(alias="domainLookupEnd")
domain_lookup_start: float = Field(alias="domainLookupStart")
duration: float
encoded_body_size: int = Field(alias="encodedBodySize")
entry_type: str = Field(alias="entryType", default="resource")
fetch_start: float = Field(alias="fetchStart")
initiator_type: str = Field(alias="initiatorType")
name: str
next_hop_protocol: str = Field(alias="nextHopProtocol")
redirect_end: float = Field(alias="redirectEnd")
redirect_start: float = Field(alias="redirectStart")
request_start: float = Field(alias="requestStart")
response_end: float = Field(alias="responseEnd")
response_start: float = Field(alias="responseStart")
secure_connection_start: float = Field(alias="secureConnectionStart")
server_timing: List = Field(alias="serverTiming")
start_time: float = Field(alias="startTime")
transfer_size: int = Field(alias="transferSize")
worker_start: float = Field(alias="workerStart")
class WebPerformance(BaseModel):
"""Pylenium's WebPerformance Object.
This is built using multiple W3C Performance Timing objects to provide
custom data points like:
* Page Load Time
* Time to First Contentful Paint
* Time to Interactive (TTI)
* and more!
"""
time_origin: float # High resolution timestamp of the start time of the Performance measurement
navigation_timing: NavigationTiming
paint_timing: PaintTiming
resources: List[ResourceTiming]
def page_load_time(self) -> float:
"""The time it takes for the page to load as experienced by the user."""
return self.navigation_timing.load_event_end - self.navigation_timing.start_time
def time_to_first_byte(self) -> float:
"""The time it takes before the first byte of response is received from the server."""
return self.navigation_timing.response_start
def time_to_first_contentful_paint(self) -> float:
"""The time it takes for the majority of content to be fully rendered and consumable by the user."""
return self.paint_timing.start_time
def time_to_interactive(self) -> float:
"""The time it takes for the layout to be stabilized and the page is responsive."""
return self.navigation_timing.dom_complete
def number_of_requests(self) -> int:
"""The number of requests sent from start of navigation until end of page load."""
return len(self.resources)
def time_to_dom_content_loaded(self) -> float:
return self.navigation_timing.dom_content_loaded_event_end
def page_weight(self) -> float:
"""The amount of bytes transferred for the page to be loaded."""
resource_transfer_size = sum([r.transfer_size for r in self.resources])
return self.navigation_timing.transfer_size + resource_transfer_size
def connection_time(self) -> float:
"""The time taken to connect to the server."""
return self.navigation_timing.connect_end - self.navigation_timing.connect_start
def request_time(self) -> float:
"""The time taken to send a request to the server and receive the response."""
return self.navigation_timing.response_end - self.navigation_timing.response_start
def fetch_time(self) -> float:
"""The time to complete the document fetch (including accessing any caches, etc.)."""
return self.navigation_timing.response_end - self.navigation_timing.fetch_start