由于最近工作一直很緊張,拖了很久才在五一假期將Selenium實現自動化頁面性能測試的代碼實現部分補上,希望今后自己能更勤勉,多一些知識產出。 Selenium WebDriver(以下簡稱SW)提供了一套用于Web應用程序的自動化測試工具。SW按其應用場景不同可以分為(1)基于HtmlUnit的無界面實現,并非驅動真實瀏覽器進行測試;(2)模擬真實輸入,對多瀏覽器的支持和測試,包括FirefoxDriver、InternetExplorerDriver、OperaDriver和ChromeDriver;(3)對移動應用的測試,包括AndroidDriver和iPhoneDriver。 針對SW進行功能性測試的文章和書已經很多了,比如如何操作獲取頁面元素內容。而本文所要寫的是如何基于Selenium和ChromeDriver做頁面性能測試,比如獲取頁面請求的加載時間、獲取頁面的DOM元素加載完成時間等等。類似于一些成熟的撥測產品的實現原型(這也是筆者正在做的項目)。我想這是非常有意義的一次探索。
首先,項目需要引入依賴的相關selenium包:selenium-api和selenium-java,要考慮不同版本和JDK版本的兼容性,筆者是JDK 1.8。
<!-- https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-api --><dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-api</artifactId> <version>3.5.3</version></dependency>
<!-- https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-java --><dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-java</artifactId> <version>3.5.3</version></dependency>
本節內容參考https://sites.google.com/a/chromium.org/chromedriver/home,另外ChromeDriver的安裝,筆者在《CentOS 7.x環境下搭建: Headless chrome + Selenium + ChromeDriver 實現自動化測試》中有詳述。
Capabilities
屬性可以定義和配置你的ChromeDriver會話,以滿足對應功能和需求。 在Java實現中,類ChromeOptions
和類DesiredCapabilities
都可以用于具體定義Capabilities
。 比如以下代碼,通過ChromeOptions
來定義Chrome的window-size屬性:
// 設置chromedriver路徑System.setProperty("webdriver.chrome.driver","/opt/drivers/chromedriver");ChromeOptions options = new ChromeOptions();// 設置chrome啟動時size大小options.addArguments("--window-size=1980,1000");// 根據ChromeOptions實例化ChromeDriverWebDriver driver = new ChromeDriver(options);try { // 打開蘇寧易購 driver.get("https://www.suning.com"); } catch (Exception e) { e.printStackTrace(); } finally { // 關閉瀏覽器 driver.quit();}
當然,以上例子也可以改寫為通過DesiredCapabilities
來實現:
// 設置chromedriver路徑System.setProperty("webdriver.chrome.driver","/opt/drivers/chromedriver");ChromeOptions options = new ChromeOptions();// 設置chrome啟動時size大小options.addArguments("--window-size=1980,1000");DesiredCapabilities cap = DesiredCapabilities.chrome();cap.setCapability(ChromeOptions.CAPABILITY, options);// 根據DesiredCapabilities實例化ChromeDriverWebDriver driver = new ChromeDriver(cap);try { // 打開蘇寧易購 driver.get("https://www.suning.com"); } catch (Exception e) { e.printStackTrace(); } finally { // 關閉瀏覽器 driver.quit();}
ChromeDriver支持性能日志(Performance Log)數據的采集。想想看Chrome的F12控制臺,我們能夠采集到”Network”、Page”等,而這些是實現頁面性能測試的基礎。 Performance Log并非是默認開啟的屬性,所以我們可以通過上節說的DesiredCapabilities
在創建新會話的時候開啟Performance Log。 而采集到的日志,我們可以通過LogEntry
對象輸出到Console。具體代碼實現如下:
package com.suning.webdrivertest.chromedemo;import org.openqa.selenium.WebDriver;import org.openqa.selenium.chrome.ChromeDriver;import org.openqa.selenium.logging.LogEntry;import org.openqa.selenium.logging.LogType;import org.openqa.selenium.logging.LoggingPreferences;import org.openqa.selenium.remote.CapabilityType;import org.openqa.selenium.remote.DesiredCapabilities;import java.util.logging.Level;/** * * Created by zhuyiquan90 on 2018/1/3. */public class ChromeDriverDemo1 { public static void main(String[] args) { // 設置chromedriver路徑 System.setProperty("webdriver.chrome.driver", "/opt/drivers/chromedriver"); DesiredCapabilities cap = DesiredCapabilities.chrome(); LoggingPreferences logPrefs = new LoggingPreferences(); // 啟用Performance Log日志采集 logPrefs.enable(LogType.PERFORMANCE, Level.ALL); cap.setCapability(CapabilityType.LOGGING_PREFS, logPrefs); // 根據DesiredCapabilities實例化ChromeDriver WebDriver driver = new ChromeDriver(cap); try { // 打開蘇寧易購 driver.get("https://www.suning.com"); for (LogEntry entry : driver.manage().logs().get(LogType.PERFORMANCE)) { // 輸出采集到的性能日志 System.out.println(Thread.currentThread().getName() + entry.toString()); } } catch (Exception e) { e.printStackTrace(); } finally { // 關閉瀏覽器 driver.quit(); } }}
其輸出結果如下:
Starting ChromeDriver 2.34.522932 (4140ab217e1ca1bec0c4b4d1b148f3361eb3a03e) on port 29777Only local connections are allowed.四月 30, 2018 3:06:27 下午 org.openqa.selenium.remote.ProtocolHandshake createSession信息: Detected dialect: OSSmain[2018-04-30T15:06:27+0800] [INFO] { "message":{ "method":"Page.frameAttached","params":{ "frameId":"49C70573CE1145CEB5B38A270213A48","parentFrameId":"28DAFE9FE90E9292F1B8EDB3315608EC","stack":{ "callFrames":[{ "columnNumber":240,"functionName":"","lineNumber":0,"scriptId":"21","url":""}]}}},"webview":"28DAFE9FE90E9292F1B8EDB3315608EC"}main[2018-04-30T15:06:27+0800] [INFO] { "message":{ "method":"Page.frameStartedLoading","params":{ "frameId":"49C70573CE1145CEB5B38A270213A48"}},"webview":"28DAFE9FE90E9292F1B8EDB3315608EC"}main[2018-04-30T15:06:27+0800] [INFO] { "message":{ "method":"Page.frameNavigated","params":{ "frame":{ "id":"49C70573CE1145CEB5B38A270213A48","loaderId":"EE699DC52C8ACA226069D24DC92E16","mimeType":"text/html","name":"chromedriver dummy frame","parentId":"28DAFE9FE90E9292F1B8EDB3315608EC","securityOrigin":"://","url":"about:blank"}}},"webview":"28DAFE9FE90E9292F1B8EDB3315608EC"}
這一節,我們來講講Network和Page包含的內容,即針對上一節輸出的內容,我們如何有效利用,通過它們來計算頁面性能(參考Chrome DevTools Protocol)。
Network中我們用到的事件主要是requestWillBeSent、responseReceived、loadingFailed和loadingFinished四種:
當頁面即將發送HTTP請求時觸發,其Json格式為:
{ "message": { "method": "Network.requestWillBeSent", "params": { "documentURL": "about:blank", "frameId": "C80F96297F4216E35079CFD86251AB8B", "initiator": { "lineNumber": 0, "type": "parser", "url": "https://www.suning.com/" }, "loaderId": "58DDB2CF16600EAE484A541DF9440089", "redirectResponse": { "connectionId": 639, "connectionReused": false, "encodedDataLength": 497, "fromDiskCache": false, "fromServiceWorker": false, "headers": { "Cache-Control": "no-cache", "Connection": "keep-alive", "Content-Length": "0", "Date": "Mon, 30 Apr 2018 07:06:42 GMT", "Expires": "Thu, 01 Jan 1970 00:00:00 GMT", "Location": "https://cm.g.doubleclick.net/pixel?google_nid=ipy&google_cm", "P3P": "CP="NON DSP COR CURa ADMa DEVa TAIa PSAa PSDa IVAa IVDa CONa HISa TELa OTPa OUR UNRa IND UNI COM NAV INT DEM CNT PRE LOC"", "Pragma": "no-cache", "Server": "nginx/1.10.2", "Set-Cookie": "CMBMP=IWl; Domain=.ipinyou.com; Expires=Thu, 10-May-2018 07:06:42 GMT; Path=/" }, "headersText": "HTTP/1.1 302 FoundrnServer: nginx/1.10.2rnDate: Mon, 30 Apr 2018 07:06:42 GMTrnContent-Length: 0rnConnection: keep-alivernCache-Control: no-cachernPragma: no-cachernExpires: Thu, 01 Jan 1970 00:00:00 GMTrnP3P: CP="NON DSP COR CURa ADMa DEVa TAIa PSAa PSDa IVAa IVDa CONa HISa TELa OTPa OUR UNRa IND UNI COM NAV INT DEM CNT PRE LOC"rnSet-Cookie: CMBMP=IWl; Domain=.ipinyou.com; Expires=Thu, 10-May-2018 07:06:42 GMT; Path=/rnLocation: https://cm.g.doubleclick.net/pixel?google_nid=ipy&google_cmrnrn", "mimeType": "", "protocol": "http/1.1", "remoteIPAddress": "127.0.0.1", "remotePort": 1086, "requestHeaders": { "Accept": "image/webp,image/apng,image/*,*/*;q=0.8", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "keep-alive", "Cookie": "sessionId=I4UF6b1WcgGMC; PYID=I4UF6b1Wcg99; CMTMS=p7Ik3Ve; CMSTMS=p7Ik3Ve; CMPUB=ADV-DefaultAdv; CMBMP=IW2", "Host": "cm.ipinyou.com", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36" }, "requestHeadersText": "GET /baidu/cms.gif?baidu_error=1×tamp=1525072001 HTTP/1.1rnHost: cm.ipinyou.comrnConnection: keep-alivernUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36rnAccept: image/webp,image/apng,image/*,*/*;q=0.8rnAccept-Encoding: gzip, deflate, brrnAccept-Language: zh-CN,zh;q=0.9rnCookie: sessionId=I4UF6b1WcgGMC; PYID=I4UF6b1Wcg99; CMTMS=p7Ik3Ve; CMSTMS=p7Ik3Ve; CMPUB=ADV-DefaultAdv; CMBMP=IW2rn", "securityDetails": { "certificateId": 0, "cipher": "AES_256_GCM", "issuer": "RapidSSL SHA256 CA", "keyExchange": "ECDHE_RSA", "keyExchangeGroup": "P-256", "protocol": "TLS 1.2", "sanList": ["*.ipinyou.com", "ipinyou.com"], "signedCertificateTimestampList": [{ "hashAlgorithm": "SHA-256", "logDescription": "Symantec log", "logId": "DDEB1D2B7A0D4FA6208B81AD8168707E2E8E9D01D55C888D3D11C4CDB6ECBECC", "origin": "Embedded in certificate", "signatureAlgorithm": "ECDSA", "signatureData": "3045022024364934CBC90A8529E327E6EF853E3EF5E48B7F1598414E0F10059DC92685FC022100A74F93A8CF23D6572D7597C072368D69EC43AFB6A9EDAA4B01B43921AADEFDC2", "status": "Verified", "timestamp": 1511173770857.0 }, { "hashAlgorithm": "SHA-256", "logDescription": "Google 'Pilot' log", "logId": "A4B90990B418581487BB13A2CC67700A3C359804F91BDFB8E377CD0EC80DDC10", "origin": "Embedded in certificate", "signatureAlgorithm": "ECDSA", "signatureData": "3046022100F319D0F56F27C82228E2B01934A1C7F46915A1509F094EE91508F08C3B5AE2B2022100B0D94DD6FD00CB435EC33B916B52EC76FE5FFCC5D5BD8CB559248243AEDFE3CE", "status": "Verified", "timestamp": 1511173770923.0 }], "subjectName": "*.ipinyou.com", "validFrom": 1511136000, "validTo": 1547942399 }, "securityState": "secure", "status": 302, "statusText": "Found", "timing": { "connectEnd": 772.852999994939, "connectStart": 0.566999995498918, "dnsEnd": -1, "dnsStart": -1, "proxyEnd": -1, "proxyStart": -1, "pushEnd": 0, "pushStart": 0, "receiveHeadersEnd": 1226.29800000141, "requestTime": 42129.997749, "sendEnd": 773.012999998173, "sendStart": 772.960999995121, "sslEnd": 772.844999999506, "sslStart": 1.62599999748636, "workerReady": -1, "workerStart": -1 }, "url": "https://cm.ipinyou.com/baidu/cms.gif?baidu_error=1×tamp=1525072001" }, "request": { "headers": { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36" }, "initialPriority": "Low", "method": "GET", "mixedContentType": "none", "referrerPolicy": "no-referrer-when-downgrade", "url": "https://cm.g.doubleclick.net/pixel?google_nid=ipy&google_cm" }, "requestId": "20524.247", "timestamp": 42131.225431, "type": "Image", "wallTime": 1525072000.35906 } }, "webview": "28DAFE9FE90E9292F1B8EDB3315608EC" }
參數說明:
參數 | 類型 | 說明 |
---|---|---|
requestId | String | 唯一請求ID |
loaderId | String | 加載ID |
documentURL | String | 頁面文檔URL |
request | Request | 請求數據對象 |
timestamp | float | 以過去某個任意時間點為基點,從打開頁面開始,以秒為單位單調遞增的時間戳 |
wallTime | float | UTC時間 |
initiator | Initiator | 請求初始化對象 |
redirectResponse | Response | 重定向響應對象 |
type | String | 資源類型 |
frameId | String | FrameID |
hasUserGesture | boolean | Whether the request is initiated by a user gesture. Defaults to false. |
其中, Request對象:
參數 | 類型 | 說明 |
---|---|---|
url | String | 請求url |
method | String | HTTP請求類型 |
headers | Object | 請求頭信息 |
postData | String | Post請求數據 |
hasPostData | boolean | 如果是Post請求,則為true |
mixedContentType | String | 是否存在混淆內容問題:blockable, optionally-blockable, none. |
initialPriority | String | 資源加載優先級:VeryLow, Low, Medium, High, VeryHigh. |
referrerPolicy | String | 跨域策略:no-referrer-when-downgrade, no-referrer, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin |
isLinkPreload | boolean | 是否通過預加載方式加載 |
Response對象:
參數 | 類型 | 說明 |
---|---|---|
url | String | 請求url |
status | int | 響應狀態碼 |
statusText | String | 狀態碼內容 |
headers | Object | 響應頭部,json格式 |
headersText | String | 響應頭部,文本格式 |
mimeType | String | Resource mimeType |
requestHeaders | Obeject | 請求頭部,json格式 |
requestHeadersText | String | 請求頭部,文本格式 |
connectionReused | boolean | 連接是否被復用 |
connectionId | long | 物理連接ID |
remoteIPAddress | String | Remote IP address |
remotePort | int | Remote port |
fromDiskCache | boolean | 是否直接從瀏覽器緩存獲取資源 |
fromServiceWorker | boolean | Specifies that the request was served from the ServiceWorker |
encodedDataLength | long | 響應字節數 |
timing | ResourceTiming | ResourceTiming對象 |
protocol | String | 協議 |
securityState | String | Security state of the request resource:unknown, neutral, insecure, secure, info |
securityDetails | SecurityDetails | Security details for the request |
ResourceTiming對象:
參數 | 類型 | 說明 |
---|---|---|
requestTime | float | 時間基線 |
proxyStart | float | Started resolving proxy. |
proxyEnd | float | Finished resolving proxy. |
dnsStart | float | Started DNS address resolve. |
dnsEnd | float | Finished DNS address resolve. |
connectStart | float | Started connecting to the remote host. |
connectEnd | float | Connected to the remote host. |
sslStart | float | Started SSL handshake. |
sslEnd | float | Finished SSL handshake. |
workerStart | float | Started running ServiceWorker. |
workerReady | float | Finished Starting ServiceWorker. |
sendStart | float | Started sending request. |
sendEnd | float | Finished sending request. |
pushStart | float | Time the server started pushing request. |
pushEnd | float | Time the server finished pushing request. |
receiveHeadersEnd | float | Finished receiving response headers. |
當HTTP響應可用時觸發,其Json格式為:
{ "message": { "method": "Network.responseReceived", "params": { "frameId": "28DAFE9FE90E9292F1B8EDB3315608EC", "loaderId": "44DBCD0BEBFCEE5AED6388366BCB719B", "requestId": "20524.277", "response": { "connectionId": 468, "connectionReused": true, "encodedDataLength": 439, "fromDiskCache": false, "fromServiceWorker": false, "headers": { "Cache-Control": "no-cache, max-age=0, must-revalidate", "Connection": "keep-alive", "Content-Length": "43", "Content-Type": "image/gif", "Date": "Mon, 30 Apr 2018 07:06:42 GMT", "Expires": "Fri, 01 Jan 1980 00:00:00 GMT", "Last-Modified": "Mon, 28 Sep 1970 06:00:00 GMT", "Pragma": "no-cache", "Server": "nginx/1.6.3", "X-Dscp-Value": "0", "X-Via": "1.1 dxun38:1 (Cdn Cache Server V2.0), 1.1 shb115:4 (Cdn Cache Server V2.0), 1.1 ls10:0 (Cdn Cache Server V2.0)" }, "headersText": "HTTP/1.1 200 OKrnDate: Mon, 30 Apr 2018 07:06:42 GMTrnServer: nginx/1.6.3rnContent-Type: image/gifrnContent-Length: 43rnLast-Modified: Mon, 28 Sep 1970 06:00:00 GMTrnExpires: Fri, 01 Jan 1980 00:00:00 GMTrnPragma: no-cachernCache-Control: no-cache, max-age=0, must-revalidaternX-Dscp-Value: 0rnX-Via: 1.1 dxun38:1 (Cdn Cache Server V2.0), 1.1 shb115:4 (Cdn Cache Server V2.0), 1.1 ls10:0 (Cdn Cache Server V2.0)rnConnection: keep-alivernrn", "mimeType": "image/gif", "protocol": "http/1.1", "remoteIPAddress": "127.0.0.1", "remotePort": 1086, "requestHeaders": { "Accept": "image/webp,image/apng,image/*,*/*;q=0.8", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "keep-alive", "Cookie": "_snstyxuid=ADFD3F4299718846; _snvd=152507199416958111", "Host": "sa.suning.cn", "Referer": "https://www.suning.com/", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36" }, "requestHeadersText": "GET /ajaxSiteExpro.gif?oId=152507199969277498&pvId=152507199147454663&expoInfo=index3_homepage1_32618013033_word03,index3_homepage1_32618013033_word04,index3_homepage1_newUser_tankuang&expoType=1&pageUrl=https://www.suning.com/&visitorId=&loginUserName=&memberID=-&sessionId=&pageType=web&hidUrlPattern=&iId=log_1525071999692 HTTP/1.1rnHost: sa.suning.cnrnConnection: keep-alivernUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36rnAccept: image/webp,image/apng,image/*,*/*;q=0.8rnReferer: https://www.suning.com/rnAccept-Encoding: gzip, deflate, brrnAccept-Language: zh-CN,zh;q=0.9rnCookie: _snstyxuid=ADFD3F4299718846; _snvd=152507199416958111rn", "securityDetails": { "certificateId": 0, "cipher": "AES_256_GCM", "issuer": "WoSign OV SSL CA", "keyExchange": "ECDHE_RSA", "keyExchangeGroup": "P-256", "protocol": "TLS 1.2", "sanList": ["*.suning.cn", "suning.cn"], "signedCertificateTimestampList": [], "subjectName": "*.suning.cn", "validFrom": 1479721356, "validTo": 1574329356 }, "securityState": "secure", "status": 200, "statusText": "OK", "timing": { "connectEnd": -1, "connectStart": -1, "dnsEnd": -1, "dnsStart": -1, "proxyEnd": -1, "proxyStart": -1, "pushEnd": 0, "pushStart": 0, "receiveHeadersEnd": 656.157999997959, "requestTime": 42130.56839, "sendEnd": 1.03800000215415, "sendStart": 0.979999997070991, "sslEnd": -1, "sslStart": -1, "workerReady": -1, "workerStart": -1 }, "url": "https://sa.suning.cn/ajaxSiteExpro.gif?oId=152507199969277498&pvId=152507199147454663&expoInfo=index3_homepage1_32618013033_word03,index3_homepage1_32618013033_word04,index3_homepage1_newUser_tankuang&expoType=1&pageUrl=https://www.suning.com/&visitorId=&loginUserName=&memberID=-&sessionId=&pageType=web&hidUrlPattern=&iId=log_1525071999692" }, "timestamp": 42131.22618, "type": "Image" } }, "webview": "28DAFE9FE90E9292F1B8EDB3315608EC" }
參數說明:
參數 | 類型 | 說明 |
---|---|---|
requestId | String | 唯一請求ID |
timestamp | float | 以過去某個任意時間點為基點,從打開頁面開始,以秒為單位單調遞增的時間戳 |
type | String | 資源類型 |
response | Response | 響應對象 |
frameId | String | FrameID |
應用場景:根據Response可以快速識別請求的各種異常狀態碼(5XX、4XX),以及時間的分布。
當HTTP請求無法加載時觸發,其Json格式為:
{ "message": { "method": "Network.loadingFailed", "params": { "canceled": true, "errorText": "net::ERR_ABORTED", "requestId": "20524.271", "timestamp": 42130.877864, "type": "Image" } }, "webview": "28DAFE9FE90E9292F1B8EDB3315608EC" }
參數說明:
參數 | 類型 | 說明 |
---|---|---|
requestId | String | 唯一請求ID |
loaderId | String | 加載ID |
timestamp | float | 以過去某個任意時間點為基點,從打開頁面開始,以秒為單位單調遞增的時間戳 |
type | String | 資源類型 |
errorText | String | 錯誤原因提示 |
canceled | boolean | 如果請求加載被取消,則為true |
blockedReason | String | 請求被阻塞的原因 |
應用場景:我們可以通過loadingFailed和requestWillBeSent確定哪些請求加載失敗。
當HTTP請求完成加載時觸發,其Json格式為:
{ "message": { "method": "Network.loadingFinished", "params": { "blockedCrossSiteDocument": false, "encodedDataLength": 327, "requestId": "20524.262", "timestamp": 42130.87542 } }, "webview": "28DAFE9FE90E9292F1B8EDB3315608EC" }
參數說明:
參數 | 類型 | 說明 |
---|---|---|
requestId | String | 唯一請求ID |
timestamp | float | 以過去某個任意時間點為基點,從打開頁面開始,以秒為單位單調遞增的時間戳 |
encodedDataLength | long | 響應字節數 |
blockedCrossSiteDocument | boolean | 如果由于跨域阻塞了響應,則為true |
應用場景:根據encodedDataLength,計算響應的最終大小。
針對requestWillBeSent、responseReceived、loadingFailed和loadingFinished四種對象,Java構建Model如下所示:
Page中我們用到的事件主要是domContentEventFired和loadEventFired兩種:
頁面Dom內容加載完成時間。
{ "message": { "method": "Page.domContentEventFired", "params": { "timestamp": 42124.003701 } }, "webview": "28DAFE9FE90E9292F1B8EDB3315608EC" }
頁面加載完成時間。
{ "message": { "method": "Page.loadEventFired", "params": { "timestamp": 42133.108263 } }, "webview": "28DAFE9FE90E9292F1B8EDB3315608EC" }
以上,我們可以根據ChromeDriver來完成對頁面加載性能分析的自動化測試了。
本節介紹ChromeDriverService
,這完全是出于提高測試性能的考慮。我們知道每次創建一個ChromeDriver,完成測試以后再釋放掉這個對象,等下次來了一個新的測試,仍要再新建一個對象,如此反復。這相當于每次都打開瀏覽器,再關閉瀏覽器,再打開瀏覽器。這種實現方式并不利于高并發的測試場景。 我們希望如Java的池化設計思想一樣,初始化生成多個持久化的瀏覽器對象,后面每次測試都用這些瀏覽器對象進行,這樣會極大提升測試性能(想想看,避免了往復創建和關閉進程的過程啊!)。因此引入ChromeDriverService
,ChromeDriverService
是一個管理ChromeDriver server的的持久化實例:
The purpose of ChromeDriverService is to manage a persistent instance of the ChromeDriver server. Standard practice is to use the ChromeDriver class or the Selenium standalone server to obtain Chrome driver instances, but this practice sacrifices performance for convenience. In this scenario, each driver instance is associated with its own instance of the ChromeDriver server, which gets launched when the driver is requested and terminated when the driver exits. This per-instance server management adds overhead to test execution, both in terms of run-time and resource utilization. Using ChromeDriverService, this overhead can be reduced to a minimum by enabling your test framework to launch a server instance at the start of the test suite and shut it down when the suite finishes. An example of this approach can be found on the ChromeDriver Getting started page under the heading Controlling ChromeDriver’s lifetime.
其使用可以參考:Java Code Examples for org.openqa.selenium.chrome.ChromeDriverService。 下面是我實現的一個Demo,我生成了3個線程分別持有一個ChromeDrvierService對象,相當于每個線程管理一個瀏覽器進程。采用阻塞隊列BQ來實現生產者-消費者模式,當隊列中有任務時,會分配給一個線程去進行測試。當隊列中無任務時,也不會銷毀ChromeDrvierService。阻塞隊列的深度和線程池的大小可以根據服務器性能動態調整。
package com.suning.webdrivertest.chromedemo;import org.openqa.selenium.WebDriver;import org.openqa.selenium.chrome.ChromeDriver;import org.openqa.selenium.chrome.ChromeDriverService;import org.openqa.selenium.logging.LogEntry;import org.openqa.selenium.logging.LogType;import org.openqa.selenium.logging.LoggingPreferences;import org.openqa.selenium.remote.CapabilityType;import org.openqa.selenium.remote.DesiredCapabilities;import java.io.File;import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.BlockingQueue;import java.util.concurrent.CountDownLatch;import java.util.logging.Level;/** * Created by zhuyiquan90 on 2018/1/9. */public class ChromeDriverDemo2 { // 阻塞隊列長度 private static final int BLOCK_QUEUE_SIZE = 100; // 瀏覽器driver線程數 private static final int THREAD_SIZE = 3; // chromedriver地址 private static final String chromedriverPath = "opt/drivers/chromedriver"; private static final BlockingQueue<String> reqQuene = new ArrayBlockingQueue<String>(BLOCK_QUEUE_SIZE); static class DriverRunnable implements Runnable { private WebDriver driver; CountDownLatch latch; public DriverRunnable(CountDownLatch latch) { ChromeDriverService chromeDriverService = new ChromeDriverService.Builder() .usingDriverExecutable(new File(chromedriverPath)) .usingAnyFreePort() .build(); DesiredCapabilities cap = DesiredCapabilities.chrome(); LoggingPreferences logPrefs = new LoggingPreferences(); logPrefs.enable(LogType.PERFORMANCE, Level.ALL); cap.setCapability(CapabilityType.LOGGING_PREFS, logPrefs); driver = new ChromeDriver(chromeDriverService, cap); this.latch = latch; } public void run() { while (true) { try { driver.get(reqQuene.take()); for (LogEntry entry : driver.manage().logs().get(LogType.PERFORMANCE)) { System.out.println(Thread.currentThread().getName() + entry.toString()); } latch.countDown(); } catch (Exception e) { e.printStackTrace(); } } } } public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(10); for (int i = 0; i < THREAD_SIZE; i++) { Thread driverThread = new Thread(new DriverRunnable(latch), "driverThread" + i); driverThread.start(); } for (int i = 0; i < 10; i++) { reqQuene.put("https://www.suning.com"); } // 使用latch.await()的目的僅僅是為Demo的輸出順序直觀,沒有其他作用 // 可以去掉latch latch.await(); }}
下面是本文的最后一部分,我想通過一個相對完整的應用實例來收官。這個實例來自于真實的應用場景,需求是采集每個頁面的如下數據: 首屏性能,包括:
以及全頁面性能,即打開頁面后完成對整個頁面的瀏覽,包括:
最終實現如下:
package com.suning.webdrivertest.chrome;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;import com.suning.webdrivertest.networkdto.NetworkLoadingFailedDTO;import com.suning.webdrivertest.networkdto.NetworkLoadingFinishedDTO;import com.suning.webdrivertest.networkdto.NetworkRequestWillBeSentDTO;import com.suning.webdrivertest.networkdto.NetworkResponseReceivedDTO;import com.suning.webdrivertest.performancedto.*;import org.openqa.selenium.JavascriptExecutor;import org.openqa.selenium.WebDriver;import org.openqa.selenium.chrome.ChromeDriver;import org.openqa.selenium.chrome.ChromeDriverService;import org.openqa.selenium.chrome.ChromeOptions;import org.openqa.selenium.logging.LogEntry;import org.openqa.selenium.logging.LogType;import org.openqa.selenium.logging.LoggingPreferences;import org.openqa.selenium.remote.CapabilityType;import org.openqa.selenium.remote.DesiredCapabilities;import java.text.MessageFormat;import java.util.ArrayList;import java.util.List;import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.BlockingQueue;import java.util.logging.Level;/** * Created by zhuyiquan90 on 2018/1/10. */public class ChromeTest { // 阻塞隊列長度 private static final int BLOCK_QUEUE_SIZE = 100; // 瀏覽器driver線程數 private static final int THREAD_SIZE = 1; // Fired when page is about to send HTTP request. public static final String NETWORK_REQUEST_WILL_BE_SENT = "Network.requestWillBeSent"; // Fired when HTTP response is available. public static final String NETWORK_RESPONSE_RECEIVED = "Network.responseReceived"; // Fired when HTTP request has finished loading. public static final String NETWORK_LOADING_FAILED = "Network.loadingFailed"; // Fired when HTTP request has failed to load. public static final String NETWORK_LOADING_FINISHED = "Network.loadingFinished"; // DOM Length JS public static final String JS_DOM_LENGTH = "return document.getElementsByTagName('*').length"; // ScrollingTop JS public static final String JS_SCROLLINGTOP = "return $(window).scrollTop( {0} * 1000)"; // Scrolling Y JS public static final String JS_SCROLLINGY = "return window.scrollY"; // Performance Timing JS public static final String JS_PERFORMANCE_TIMING = "return performance.timing"; private static final BlockingQueue<String> reqQuene = new ArrayBlockingQueue<String>(BLOCK_QUEUE_SIZE); static class DriverRunnable implements Runnable { private WebDriver driver; public DriverRunnable() { System.setProperty(ChromeDriverService.CHROME_DRIVER_LOG_PROPERTY, System.getProperty("user.dir") + "/target/chromedriver.log"); System.setProperty(ChromeDriverService.CHROME_DRIVER_EXE_PROPERTY, System.getProperty("user.dir") + "/drivers/chromedriver"); ChromeDriverService chromeDriverService = new ChromeDriverService.Builder() .withVerbose(true) .usingAnyFreePort() .build(); ChromeOptions options = new ChromeOptions(); // options.addArguments("--headless"); options.addArguments("--window-size=1980,1000"); options.addArguments("--disable-web-security"); // options.addArguments("--start-fullscreen"); // options.addArguments("--screenshot"); // options.addArguments("--golden-screenshots-dir=" + chromedriverPath); DesiredCapabilities cap = DesiredCapabilities.chrome(); LoggingPreferences logPrefs = new LoggingPreferences(); logPrefs.enable(LogType.PERFORMANCE, Level.ALL); cap.setCapability(CapabilityType.LOGGING_PREFS, logPrefs); cap.setCapability(ChromeOptions.CAPABILITY, options); driver = new ChromeDriver(chromeDriverService, cap); } public void run() { while (true) { try { String url = reqQuene.take(); TotalPerformanceDTO totalPerformance = new TotalPerformanceDTO(); totalPerformance.setFirstScreenPerformance( detectFirstScreenPerformance(url, driver)); totalPerformance.setFullPagePerformance( detectFullPagePerformance(url, driver)); System.out.println(totalPerformance.toString()); // 滑動頁面,直到頁面底部// long scrollStart = 0, scrollEnd = 1;// int i = 1;// while (scrollStart != scrollEnd) { // scrollStart = (Long) js.executeScript("return window.scrollY");// String scrollTo = "return $(window).scrollTop(" + i++ + " * 1000)";// js.executeScript(scrollTo);// Thread.sleep(200);// scrollEnd = (Long) js.executeScript("return window.scrollY");// System.out.println(scrollTo + ":" + scrollEnd);// }// System.out.println(Thread.currentThread().getName() + ": " +// js.executeScript("return document.getElementsByTagName('*').length"));// for (LogEntry entry : driver.manage().logs().get(LogType.PERFORMANCE)) { // System.out.println(Thread.currentThread().getName() + entry.getMessage());// } } catch (Exception e) { e.printStackTrace(); } } } } /** * 首屏數據統計 * * @param url * @param driver * @return */ private static FirstScreenPerformanceDTO detectFirstScreenPerformance(String url, WebDriver driver) { driver.get(url); // js操作對象 JavascriptExecutor js = (JavascriptExecutor) driver; FirstScreenPerformanceDTO firstScreenPerformance = new FirstScreenPerformanceDTO(); List<NetworkRequestWillBeSentDTO> firstscreenRequestList = new ArrayList<NetworkRequestWillBeSentDTO>(); List<NetworkResponseReceivedDTO> firstscreenResponseList = new ArrayList<NetworkResponseReceivedDTO>(); List<NetworkLoadingFailedDTO> firstscreenFailList = new ArrayList<NetworkLoadingFailedDTO>(); List<NetworkLoadingFinishedDTO> firstscreenFinishedList = new ArrayList<NetworkLoadingFinishedDTO>(); int pageRequestNum = 0; double pageSize = 0.0; for (LogEntry entry : driver.manage().logs().get(LogType.PERFORMANCE)) { JSONObject jsonObj = JSON.parseobject(entry.getMessage()).getJSONObject("message"); String method = jsonObj.getString("method"); String params = jsonObj.getString("params"); if (method.equals(NETWORK_REQUEST_WILL_BE_SENT)) { NetworkRequestWillBeSentDTO request = JSON.parseObject( params, NetworkRequestWillBeSentDTO.class); pageRequestNum++;// System.out.println(method + ":" + request.toString()); firstscreenRequestList.add(request); } else if (method.equals(NETWORK_RESPONSE_RECEIVED)) { NetworkResponseReceivedDTO response = JSON.parseObject( params, NetworkResponseReceivedDTO.class);// System.out.println(method + ":" + response.getResponse().getUrl()); firstscreenResponseList.add(response); } else if (method.equals(NETWORK_LOADING_FINISHED)) { NetworkLoadingFinishedDTO finished = JSON.parseObject( params, NetworkLoadingFinishedDTO.class); pageSize += finished.getEncodedDataLength();// System.out.println(method + ":" + finished.toString()); firstscreenFinishedList.add(finished); } else if (method.equals(NETWORK_LOADING_FAILED)) { NetworkLoadingFailedDTO failed = JSON.parseObject( params, NetworkLoadingFailedDTO.class);// System.out.println(method + ":" + failed.toString()); firstscreenFailList.add(failed); } } // 獲取首屏DOM數 firstScreenPerformance.setPageDomNum(executeDomLengthJS(js)); // 獲取首屏DOM加載完成時間 和 首屏完全加載完成時間 PerformanceTimingDTO performanceTiming = executePerformanceTimingJS(js); firstScreenPerformance.setDomContentLoadedCost( performanceTiming.getDomContentLoadedEventEnd() - performanceTiming.getConnectStart()); firstScreenPerformance.setLoadEventCost( performanceTiming.getLoadEventEnd() - performanceTiming.getConnectStart()); // 獲取首屏大小 firstScreenPerformance.setPageSize(pageSize / (1000 * 1000)); // 獲取首屏請求數 firstScreenPerformance.setPageRequestNum(pageRequestNum); System.out.println("頁面" + url + ":首屏請求數:" + firstScreenPerformance.getPageRequestNum() + ", 首屏大小:" + firstScreenPerformance.getPageSize() + "MB, 首屏DOM總數:" + firstScreenPerformance.getPageDomNum() + ", 首屏DOM加載完成時間:" + firstScreenPerformance.getDomContentLoadedCost() + "ms, 首屏完全加載完成時間:" + firstScreenPerformance.getLoadEventCost() + "ms"); // 分析異常響應 PageErrorsDTO pageErrorsDTO = new PageErrorsDTO(); pageErrorsDTO.setCodeErrorResponseList( analysisCodeErrorResponse(firstscreenResponseList)); if (!pageErrorsDTO.getCodeErrorResponseList().isempty()) { System.out.println("首屏異常響應:"); for (int i = 0; i < pageErrorsDTO.getCodeErrorResponseList().size(); i++) { System.out.println(pageErrorsDTO. getCodeErrorResponseList().get(i).getUrl() + ": " + pageErrorsDTO. getCodeErrorResponseList().get(i).getStatus()); } } // 分析失敗響應 pageErrorsDTO.setFailResponseList( analysisFailResponse(firstscreenFailList, firstscreenRequestList)); if (!pageErrorsDTO.getFailResponseList().isEmpty()) { System.out.println("首屏失敗響應:"); for (int i = 0; i < pageErrorsDTO.getFailResponseList().size(); i++) { System.out.println(pageErrorsDTO. getFailResponseList().get(i).getUrl() + ": " + pageErrorsDTO. getFailResponseList().get(i).getErrorText() + " " + pageErrorsDTO. getFailResponseList().get(i).getBlockedReason()); } } // 分析慢響應 pageErrorsDTO.setSlowReponseList( analysisSlowResponse(firstscreenRequestList, firstscreenFinishedList, 3.0)); if (!pageErrorsDTO.getSlowReponseList().isEmpty()) { System.out.println("首屏慢響應:"); for (int i = 0; i < pageErrorsDTO.getSlowReponseList().size(); i++) { System.out.println(pageErrorsDTO. getSlowReponseList().get(i).getUrl() + ": " + pageErrorsDTO. getSlowReponseList().get(i).getCost()); } } firstScreenPerformance.setPageErrorsDTO(pageErrorsDTO); return firstScreenPerformance; } /** * 全頁面數據統計 * * @param url * @param driver * @return */ private static FullPagePerformanceDTO detectFullPagePerformance(String url, WebDriver driver) { driver.get(url); // js操作對象 JavascriptExecutor js = (JavascriptExecutor) driver; FullPagePerformanceDTO fullPagePerformance = new FullPagePerformanceDTO(); // 滾動到頁面底部 scrollToBottom(js); List<NetworkRequestWillBeSentDTO> fullPageRequestList = new ArrayList<NetworkRequestWillBeSentDTO>(); List<NetworkResponseReceivedDTO> fullPageResponseList = new ArrayList<NetworkResponseReceivedDTO>(); List<NetworkLoadingFailedDTO> fullPageFailList = new ArrayList<NetworkLoadingFailedDTO>(); List<NetworkLoadingFinishedDTO> fullPageFinishedList = new ArrayList<NetworkLoadingFinishedDTO>(); int pageRequestNum = 0; double pageSize = 0.0; for (LogEntry entry : driver.manage().logs().get(LogType.PERFORMANCE)) { JSONObject jsonObj = JSON.parseObject(entry.getMessage()).getJSONObject("message"); String method = jsonObj.getString("method"); String params = jsonObj.getString("params"); if (method.equals(NETWORK_REQUEST_WILL_BE_SENT)) { NetworkRequestWillBeSentDTO request = JSON.parseObject( params, NetworkRequestWillBeSentDTO.class); pageRequestNum++;// System.out.println(method + ":" + request.toString()); fullPageRequestList.add(request); } else if (method.equals(NETWORK_RESPONSE_RECEIVED)) { NetworkResponseReceivedDTO response = JSON.parseObject( params, NetworkResponseReceivedDTO.class);// System.out.println(method + ":" + response.getResponse().getUrl()); fullPageResponseList.add(response); } else if (method.equals(NETWORK_LOADING_FINISHED)) { NetworkLoadingFinishedDTO finished = JSON.parseObject( params, NetworkLoadingFinishedDTO.class); pageSize += finished.getEncodedDataLength();// System.out.println(method + ":" + finished.toString()); fullPageFinishedList.add(finished); } else if (method.equals(NETWORK_LOADING_FAILED)) { NetworkLoadingFailedDTO failed = JSON.parseObject( params, NetworkLoadingFailedDTO.class);// System.out.println(method + ":" + failed.toString()); fullPageFailList.add(failed); } } // 獲取全頁面DOM數 fullPagePerformance.setPageDomNum(executeDomLengthJS(js)); // 獲取全頁面DOM加載完成時間 和 首屏完全加載完成時間 PerformanceTimingDTO performanceTiming = executePerformanceTimingJS(js); fullPagePerformance.setDomContentLoadedCost( performanceTiming.getDomContentLoadedEventEnd() - performanceTiming.getConnectStart()); fullPagePerformance.setLoadEventCost( performanceTiming.getLoadEventEnd() - performanceTiming.getConnectStart()); // 獲取全頁面大小 fullPagePerformance.setPageSize(pageSize / (1000 * 1000)); // 獲取全頁面請求數 fullPagePerformance.setPageRequestNum(pageRequestNum); System.out.println("頁面" + url + ":全頁面請求數:" + fullPagePerformance.getPageRequestNum() + ", 全頁面大小:" + fullPagePerformance.getPageSize() + "MB, 全頁面DOM總數:" + fullPagePerformance.getPageDomNum() + ", 全頁面DOM加載完成時間:" + fullPagePerformance.getDomContentLoadedCost() + "ms, 全頁面完全加載完成時間:" + fullPagePerformance.getLoadEventCost() + "ms"); // 分析異常響應 PageErrorsDTO pageErrorsDTO = new PageErrorsDTO(); pageErrorsDTO.setCodeErrorResponseList( analysisCodeErrorResponse(fullPageResponseList)); if (!pageErrorsDTO.getCodeErrorResponseList().isEmpty()) { System.out.println("全頁面異常響應:"); for (int i = 0; i < pageErrorsDTO.getCodeErrorResponseList().size(); i++) { System.out.println(pageErrorsDTO. getCodeErrorResponseList().get(i).getUrl() + ": " + pageErrorsDTO. getCodeErrorResponseList().get(i).getStatus()); } } // 分析失敗響應 pageErrorsDTO.setFailResponseList( analysisFailResponse(fullPageFailList, fullPageRequestList)); if (!pageErrorsDTO.getFailResponseList().isEmpty()) { System.out.println("全頁面失敗響應:"); for (int i = 0; i < pageErrorsDTO.getFailResponseList().size(); i++) { System.out.println(pageErrorsDTO. getFailResponseList().get(i).getUrl() + ": " + pageErrorsDTO. getFailResponseList().get(i).getErrorText() + " " + pageErrorsDTO. getFailResponseList().get(i).getBlockedReason()); } } // 分析慢響應 pageErrorsDTO.setSlowReponseList( analysisSlowResponse(fullPageRequestList, fullPageFinishedList, 3.0)); if (!pageErrorsDTO.getSlowReponseList().isEmpty()) { System.out.println("全頁面慢響應:"); for (int i = 0; i < pageErrorsDTO.getSlowReponseList().size(); i++) { System.out.println(pageErrorsDTO. getSlowReponseList().get(i).getUrl() + ": " + pageErrorsDTO. getSlowReponseList().get(i).getCost()); } } fullPagePerformance.setPageErrorsDTO(pageErrorsDTO); return fullPagePerformance; } /** * 滾動到頁面底部 * * @param js * @return */ private static long scrollToBottom(JavascriptExecutor js) { long scrollStart = 0, scrollEnd = 1; int i = 1; while (scrollStart != scrollEnd) { scrollStart = (Long) js.executeScript(JS_SCROLLINGY); String scrollTo = MessageFormat.format(JS_SCROLLINGTOP, i++); System.out.println(scrollTo); js.executeScript(scrollTo); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } scrollEnd = (Long) js.executeScript(JS_SCROLLINGY); // System.out.println(scrollTo + ":" + scrollEnd); } return scrollEnd; } /** * 執行js,獲取頁面DOM數 * * @param js * @return */ private static long executeDomLengthJS(JavascriptExecutor js) { return (Long) js.executeScript(JS_DOM_LENGTH); } /** * 執行js,獲取Performance Timing * * @param js * @return */ private static PerformanceTimingDTO executePerformanceTimingJS(JavascriptExecutor js) { String performance = js.executeScript(JS_PERFORMANCE_TIMING).toString(); performance = performance.replace("unloadEventEnd=", ""unloadEventEnd":") .replace("responseEnd=", ""responseEnd":") .replace("responseStart=", ""responseStart":") .replace("domInteractive=", ""domInteractive":") .replace("domainLookupEnd=", ""domainLookupEnd":") .replace("unloadEventStart=", ""unloadEventStart":") .replace("domComplete=", ""domComplete":") .replace("domContentLoadedEventStart=", ""domContentLoadedEventStart":") .replace("domainLookupStart=", ""domainLookupStart":") .replace("redirectEnd=", ""redirectEnd":") .replace("redirectStart=", ""redirectStart":") .replace("connectEnd=", ""connectEnd":") .replace("toJSON={},", "") .replace("connectStart=", ""connectStart":") .replace("loadEventStart=", ""loadEventStart":") .replace("navigationStart=", ""navigationStart":") .replace("requestStart=", ""requestStart":") .replace("secureConnectionStart=", ""secureConnectionStart":") .replace("fetchStart=", ""fetchStart":") .replace("domContentLoadedEventEnd=", ""domContentLoadedEventEnd":") .replace("domLoading=", ""domLoading":") .replace("loadEventEnd=", ""loadEventEnd":");// System.out.println(performance); return JSON.parseObject( performance, PerformanceTimingDTO.class); } /** * 分析異常狀態碼的響應 * * @param networkResponseReceivedList * @return */ private static List<CodeErrorResponseDTO> analysisCodeErrorResponse (List<NetworkResponseReceivedDTO> networkResponseReceivedList) { List<CodeErrorResponseDTO> codeErrorResponseList = new ArrayList<CodeErrorResponseDTO>(); for (NetworkResponseReceivedDTO r : networkResponseReceivedList) { if (r.getResponse().getStatus() >= 400 && r.getResponse().getStatus() <= 599) { CodeErrorResponseDTO codeErrorResponseDTO = new CodeErrorResponseDTO(); codeErrorResponseDTO.setUrl(r.getResponse().getUrl()); codeErrorResponseDTO.setStatus(r.getResponse().getStatus()); System.out.println(r.toString()); codeErrorResponseList.add(codeErrorResponseDTO); } } return codeErrorResponseList; } /** * 分析失敗的響應 * * @param networkLoadingFailedList * @param networkRequestWillBeSentList * @return */ private static List<FailResponseDTO> analysisFailResponse( List<NetworkLoadingFailedDTO> networkLoadingFailedList, List<NetworkRequestWillBeSentDTO> networkRequestWillBeSentList) { List<FailResponseDTO> failResponseList = new ArrayList<FailResponseDTO>(); for (NetworkLoadingFailedDTO f : networkLoadingFailedList) { for (NetworkRequestWillBeSentDTO r : networkRequestWillBeSentList) { if (f.getRequestId().equals(r.getRequestId())) { FailResponseDTO failResponseDTO = new FailResponseDTO(); failResponseDTO.setUrl(r.getRequest().getUrl()); failResponseDTO.setErrorText(f.getErrorText()); failResponseDTO.setBlockedReason(f.getBlockedReason()); failResponseList.add(failResponseDTO); } } } return failResponseList; } /** * 分析慢響應,單位s * * @param networkRequestWillBeSentList * @param networkLoadingFinishedList * @param slowThreshold * @return */ private static List<SlowReponseDTO> analysisSlowResponse( List<NetworkRequestWillBeSentDTO> networkRequestWillBeSentList, List<NetworkLoadingFinishedDTO> networkLoadingFinishedList, double slowThreshold) { List<SlowReponseDTO> slowReponseList = new ArrayList<SlowReponseDTO>(); for (NetworkRequestWillBeSentDTO r : networkRequestWillBeSentList) { for (NetworkLoadingFinishedDTO f : networkLoadingFinishedList) { if (r.getRequestId().equals(f.getRequestId())) { double cost = f.getTimestamp() - r.getTimestamp(); if (cost >= slowThreshold) { SlowReponseDTO slowReponseDTO = new SlowReponseDTO(); slowReponseDTO.setUrl(r.getRequest().getUrl()); slowReponseDTO.setCost(cost); slowReponseList.add(slowReponseDTO); } } } } return slowReponseList; } public static void main(String[] args) throws InterruptedException { for (int i = 0; i < THREAD_SIZE; i++) { Thread driverThread = new Thread(new DriverRunnable(), "driverThread" + i); driverThread.start(); } for (int i = 0; i < 1; i++) { reqQuene.put("https://www.suning.com"); } }}
158899.html
本文由 貴州做網站公司 整理發布,部分圖文來源于互聯網,如有侵權,請聯系我們刪除,謝謝!
網絡推廣與網站優化公司(網絡優化與推廣專家)作為數字營銷領域的核心服務提供方,其價值在于通過技術手段與策略規劃幫助企業提升線上曝光度、用戶轉化率及品牌影響力。這...
在當今數字化時代,公司網站已成為企業展示形象、傳遞信息和開展業務的重要平臺。然而,對于許多公司來說,網站建設的價格是一個關鍵考量因素。本文將圍繞“公司網站建設價...
在當今的數字化時代,企業網站已成為企業展示形象、吸引客戶和開展業務的重要平臺。然而,對于許多中小企業來說,高昂的網站建設費用可能會成為其發展的瓶頸。幸運的是,隨...
mdf文件是什么格式文件?打開方法?MDF文件是Microsoft SQL server使用的主要數據庫文件格式。企業數據庫程序用于安裝SQL server和相關附加組件的數據庫文件。用戶還可以創建自定義MDF文件。所以您可以使用Microsoft SQL Server軟件打開MDF文件。以下是具體的演示步驟:1。打開Microsoft SQL Server軟件后,右鍵單擊Microsoft SQ...
北京到成都動車最快幾小時?北京到成都有最快的高鐵,G309到成都東,8:23到2:09。運行14小時46分鐘,沒有火車。Z91小時11分,11336028是8:39,價格是高鐵的一半。北京復興號動車到成都途經哪些站?答:目前北京到成都的高鐵有5趟,??空军c有北京西、石家莊站、保定站、涿州東站、邢臺站、邯鄲東站、安陽東站、鶴壁東站、新鄉東站、澠池南站、鄭州東站、洛陽龍門站、靈寶西站、洋縣西站、平頂山...
如何統計excel表格每人每月出勤天數?在win7中,以excel 2007為例,可以參考以下步驟在excel表格中統計每人每月的出勤天數:1.首先,點擊Excel軟件,打開如圖所示的Exc考勤表中代表出勤半天,那么如何讓在表格最后邊,自動生成出勤天數?用COUNTIF函數統計的個數就可以了。例:COUNTIF(A1:A30,)。怎么用excel計算考勤?如:遲到時間、早退時間、加班時間?在E2中...