使用 W3C Performance 對象經過 R 和 JavaScript 將瀏覽器內的性能數據可視化[轉]

當考慮 Web 性能指標時,須要關注的目標數字應該是從您本身的用戶那裏得到的實際用戶指標。最多見的方法是利用 Splunk 之類的工具來分析您的機器數據,該工具支持您分析和可視化您的訪問權限和錯誤日誌。利用這些工具,您能夠收集某些方面的性能數據,好比讀取資產的文件 I/O 時間,以及 API 請求的訪問時間。可是,您仍然須要推斷客戶端性能數據,將信號調用方在某些高級的檢查點上,或者只利用相似 WebPagetest 的工具運行綜合測試。如今,W3C 已將 API 標準化,用戶能夠經過使用 Performance 對象(該對象對於全部現代瀏覽器中的 Windows 對象而言是一個本機對象)捕獲並報告瀏覽器內的性能數據。

javascript

捕獲並報告瀏覽器內性能數據的 API

在 2010 年年底,萬維網聯盟 (W3C) 創建了一個新的工做組,即 Web 性能工做組,該工做組提供了用來測量用戶代理特性和 API 的應用程序性能的各個方面的方法。該小組還開發了一個支持將瀏覽器暴露給 JavaScript 的 API,這是一個關鍵的 Web 性能指標。php

在這個 API 中,該工做組建立了大量的新對象和事件,可量化性能指標和優化性能。總的說來,這些對象和界面包括:html

  • Performance 對象:暴露多個對象,好比 PerformanceNavigationPerformanceTiming 和MemoryInfo,並能記錄高精度時間(high resolution time),從而得到亞毫秒級計時。
  • Page Visibility API:使您可以肯定某個給定頁面是可見的仍是隱藏的,從而可以優化動畫的內存使用,或優化用於輪詢操做的網絡資源。

使用這些對象和界面捕獲瀏覽器內的性能指標並將它們可視化。java

Performance 對象

若是在 JavaScript 控制檯中鍵入 window.performance,則會返回一個類型爲 Performance 的對象,以及該對象所暴露的一些對象和方法。目前,標準的對象集包含:git

  • window.performance.timing 用於類型 PerformanceTiming
  • window.performance.navigation 用於類型 PerformanceNavigation
  • window.performance.memory 用於類型 MemoryInfo(僅適用於 Chrome 瀏覽器)

圖 1 顯示了 Performance 對象的屏幕截圖,可展開該對象來顯示 PerformanceTiming 對象及其屬性。github

圖 1. Performance 對象

已擴展的 Performance 對象

Performance 對象被顯示在控制檯中,隨它一塊兒顯示的還有展開的 PerformanceTiming 對象。web

PerformanceTiming 對象

PerformanceTiming 對象是以公共屬性的形式暴露的,在瀏覽器中執行檢索和呈現內容的步驟中,它是一個關鍵指標。表 1 顯示了與PerformanceTiming 對象中的每個屬性相對應的描述。chrome

表 1. PerformanceTiming 對象屬性
對象屬性 描述
navigationStart 在導航開始的時候、在瀏覽器開始卸載前一頁(若是有這樣的頁面)的時候,或者在開始提取內容的時候,捕獲所需的數據。它將包含 unloadEventStart 數據或 fetchStart 數據。要想跟蹤端到端的時間,可從使用這個值開始。
unloadEventStartunloadEventEnd 在瀏覽器開始卸載前一頁或已完成前一頁的卸載的時候,捕獲所需的數據(若是相同域中有前一頁須要卸載的話)。
domainLookupStartdomainLookupEnd 在瀏覽器開始和完成針對所請求內容的 DNS 查找時,捕獲所需的數據。
redirectStart/redirectEnd 在瀏覽器開始和完成任何 HTTP 重定向時捕獲所需的數據。
connectStart/connectEnd 在瀏覽器開始和完成創建當前頁面的遠程服務器 TCP 鏈接時捕獲所需的數據。
fetchStart 在瀏覽器首次開始檢查用於所請求資源的緩存時捕獲所需的數據。
requestStart 在瀏覽器經過發送 HTTP 請求來得到所請求的資源時捕獲所需的數據。
responseStart/responseEnd 在瀏覽器首次進行註冊並完成註冊收到服務器響應時捕獲所需的數據。
domLoading/domComplete 在文檔開始和完成加載時捕獲所需的數據。
domContentLoadedEventEnd/domContentLoadedEventStart 在文檔的 DOMContentLoaded 開始和完成加載時捕獲所需的數據,這至關於瀏覽器已完成全部內容的加載並運行頁面中包含的全部腳本。
domInteractive 在頁面的 Document.readyState 屬性變爲 interactive 時捕獲所需的數據,這會致使觸發 readystatechange 事件。
loadEventStart/loadEventEnd 在加載事件觸發前和加載事件觸發後馬上捕獲所需的數據。

要想將上述步驟及其相應內容的順序更好地可視化,請參見圖 2。編程

圖 2. 可視化 PerformanceTiming 屬性的順序

可視化 PerformanceTiming 屬性的順序

 

Performance 導航

圖 3 顯示了包含已展開的 PerformanceNavigation 對象的 Performance 對象。json

圖 3. PerformanceNavigation 對象

PerformanceNavigation 對象

請注意,導航對象有兩個只讀屬性:redirectCount 和 type。顧名思義,redirectCount 屬性是 HTTP 重定向的數量,瀏覽器根據它們來獲取當前頁面。

HTTP 重定向是 Web 性能的一個重要因素,由於它們會致使每一次重定向都須要執行一次完整的 HTTP 往返過程。原始請求是從 Web 服務器返回的,做爲包含新位置路徑的 301 或 302。而後,瀏覽器必須初始化一個新的 TCP 鏈接,併發送一個新請求來得到新位置。這一附加步驟爲原始資源請求增長了額外的延遲。

redirectCount 屬性如清單 1 所示。

清單 1. 訪問 redirectCount 屬性
>>> performance.navigation.redirectCount
0

導航對象的另外一個屬性是 typenavigation.type 屬性是用下列常量表示的 4 個值中的一個:

  • TYPE_NAVIGATE 的值爲 0,表示可經過單擊一個連接、提交表單或直接在地址欄中輸入 URL 導航到當前頁面。
  • TYPE_RELOAD 的值爲 1,表示經過重載操做到達當前頁面。
  • TYPE_BACK_FORWARD 的值爲 2,表示經過使用瀏覽器歷史記錄、使用 back 或 forward 按鈕、以編程方式,或者經過瀏覽器的歷史對象來導航到頁面。
  • TYPE_RESERVED 的值爲 255,它是其餘任何導航類型的全方位指示。

信息彙總

要想使用這些對象來捕獲和可視化客戶端性能指標,可先建立一個 JavaScript 庫,收集 PerformanceTiming 數據,並將這些數據發送到某個端點進行收集和分析。查看這個 JavaScript 庫,該庫剛好用於完成這項工做。

perfLogger.js 腳本使用了 Performance 對象。建立一個名爲 perfLogger 的命名空間,並聲明一些局部變量來保存根據 PerformanceTiming 屬性推測的值。

您能夠經過使用這些示例和下面這些模式來計算時間:

  • 要計算認知時間(perceived time) — 從 timing.navigationStart 中減去當前時間。
  • 要計算到達頁面過程當中所經歷的全部重定向時間 — 從 timing.redirectStart 中減去 timing.redirectEnd
  • 要想得到執行 DNS 查找所用的時間 — 從 timing.domainLookupStart 中減去 timing.domainLookupEnd,要想得到呈現頁面所用的時間,請從 xs 中減去當前時間。

聲明並初始化局部變量後,使用 public getter 函數從命名空間暴露它們,如清單 2 所示。

清單 2. 暴露 public getter 函數的局部變量

var perfLogger = function(){
    var serverLogURL = "/lib/savePerfData.php",
    loggerPool = [],
    _pTime = Date.now() - performance.timing
.navigationStart || 0,
    _redirTime = performance.timing.redirectEnd 
- performance.timing.redirectStart || 0,
        _cacheTime = performance.timing.domainLookupStart 
- performance.timing.fetchStart || 0,
        _dnsTime = performance.timing.domainLookupEnd 
- performance.timing.domainLookupStart || 0,
        _tcpTime = performance.timing.connectEnd 
- performance.timing.connectStart || 0,
        _roundtripTime = performance.timing.responseEnd 
- performance.timing.connectStart || 0,
        _renderTime = Date.now() - performance.timing
.domLoading || 0;

//expose derived performance data
    perceivedTime: function(){
        return _pTime;
    }, 
    redirectTime: function(){
        _redirTime;
    }, 
    cacheTime: function(){
        return _cacheTime;
    }, 
    dnsLookupTime: function(){
        return _dnsTime;
    },
    tcpConnectionTime: function(){
        return _tcpTime;
    },
    roundTripTime: function(){
        return _roundtripTime;
    },
    pageRenderTime: function(){
        return _renderTime;
    },
    

}

 

您能夠從命名空間訪問屬性,如清單 3 所示。

清單 3. 從命名空間訪問屬性

perfLogger.pageRenderTime
perfLogger. roundTripTime
perfLogger. tcpConnectionTime
perfLogger. dnsLookupTime
perfLogger. cacheTime
perfLogger. redirectTime
perfLogger. perceivedTime

 

在命名空間中,函數 logToServer 將指標從新寫回您在變量 serverLogURL 中定義的端點,如清單 4 所示。

清單 4. logToServer 函數
function logToServer(id){
            var params = "data=" + JSON.stringify(jsonConcat
(loggerPool[id],TestResults.prototype));
            console.log(params)
            var xhr = new XMLHttpRequest();
            xhr.open("POST", serverLogURL, true);
            xhr.setRequestHeader("Content-type", 
"application/x-www-form-urlencoded");
            xhr.setRequestHeader("Content-length", params.length);
            xhr.setRequestHeader("Connection", "close");
            xhr.onreadystatechange = function()
              {
            if (xhr.readyState==4 && xhr.status==200)
            {
               console.log('log written');
            }
         };
       xhr.send(params);    
    }

 

perfLogger.js 庫還提供了一些基準測試功能,您能夠在其中測試 JavaScript 的專用數據塊,甚至能夠運行一組花費 N 時間量的測試來執行真正的基準測試。

perfLogger.js 庫的完整源代碼如清單 5 所示。

清單 5. perfLogger.js 庫的完整源代碼
var perfLogger = function(){
    var serverLogURL = "/lib/savePerfData.php",
        loggerPool = [],
        _pTime = Date.now() - performance.timing.navigationStart 
|| 0,
        _redirTime = performance.timing.redirectEnd 
- performance.timing.redirectStart || 0,
        _cacheTime = performance.timing.domainLookupStart 
- performance.timing.fetchStart || 0,
        _dnsTime = performance.timing.domainLookupEnd 
- performance.timing.domainLookupStart || 0,
        _tcpTime = performance.timing.connectEnd 
- performance.timing.connectStart || 0,
        _roundtripTime = performance.timing.responseEnd 
- performance.timing.connectStart || 0,
        _renderTime = Date.now() - performance.timing.domLoading 
|| 0;
        
        function TestResults(){};
        TestResults.prototype.perceivedTime = _pTime;
        TestResults.prototype.redirectTime = _redirTime;
        TestResults.prototype.cacheTime = _cacheTime;
        TestResults.prototype.dnsLookupTime = _dnsTime;
        TestResults.prototype.tcpConnectionTime = _tcpTime;
        TestResults.prototype.roundTripTime = _roundtripTime;
        TestResults.prototype.pageRenderTime = _renderTime;
        
        function jsonConcat(object1, object2) {
         for (var key in object2) {
          object1[key] = object2[key];
         }
         return object1;
        }
                            
        function calculateResults(id){
            loggerPool[id].runtime = loggerPool[id].stopTime 
- loggerPool[id].startTime;
        }
        
        function setResultsMetaData(id){
            loggerPool[id].url = window.location.href;
            loggerPool[id].useragent = navigator.userAgent;
        }
        
        function drawToDebugScreen(id){
            var debug = document.getElementById("debug")
            var output = formatDebugInfo(id)
            if(!debug){
                var divTag = document.createElement("div");
                divTag.id = "debug";
                divTag.innerHTML = output
                document.body.appendChild(divTag);           
            }else{
                debug.innerHTML += output
            }
        }

        function logToServer(id){
            var params = "data=" + JSON.stringify(jsonConcat(
    loggerPool[id],TestResults.prototype));
            console.log(params)
            var xhr = new XMLHttpRequest();
            xhr.open("POST", serverLogURL, true);
            xhr.setRequestHeader("Content-type", 
"application/x-www-form-urlencoded");
            xhr.setRequestHeader("Content-length", params.length);
            xhr.setRequestHeader("Connection", "close");
            xhr.onreadystatechange = function()
              {
              if (xhr.readyState==4 && xhr.status==200)
                {
                    //console.log(xhr.responseText);
                }
              };
            xhr.send(params);    
        }
        
        function formatDebugInfo(id){
            var debuginfo = "<p><strong>" 
+ loggerPool[id].description + "</strong><br/>";    
            if(loggerPool[id].avgRunTime){
                debuginfo += "average run time: " + loggerPool[id]
.avgRunTime + "ms<br/>";
            }else{
                debuginfo += "run time: " + loggerPool[id].runtime 
+ "ms<br/>";
            }
            debuginfo += "path: " + loggerPool[id].url 
+ "<br/>";
            debuginfo += "useragent: " +  loggerPool[id].useragent 
+ "<br/>";
            
            debuginfo += "Perceived Time: " + 
loggerPool[id].perceivedTime + "<br/>";
            debuginfo += "Redirect Time: " + 
loggerPool[id].redirectTime + "<br/>";
            debuginfo += "Cache Time: " + 
loggerPool[id].cacheTime + "<br/>";
            debuginfo += "DNS Lookup Time: " + 
loggerPool[id].dnsLookupTime + "<br/>";
            debuginfo += "tcp Connection Time: " + 
loggerPool[id].tcpConnectionTime + "<br/>";
            debuginfo += "roundTripTime: "+ 
loggerPool[id].roundTripTime + "<br/>";
            debuginfo += "pageRenderTime: " + 
loggerPool[id].pageRenderTime + "<br/>";
            debuginfo += "</p>";
            return debuginfo
        }
        
    return {        
    startTimeLogging: function(id, descr,drawToPage
,logToServer){
        loggerPool[id] = new TestResults();
        loggerPool[id].id = id;
        loggerPool[id].startTime =  performance.now();
        loggerPool[id].description = descr;
        loggerPool[id].drawtopage = drawToPage;
        loggerPool[id].logtoserver = logToServer
    },
    
    stopTimeLogging: function(id){
        loggerPool[id].stopTime =  performance.now();
        calculateResults(id);
        setResultsMetaData(id);    
        if(loggerPool[id].drawtopage){
            drawToDebugScreen(id);
        }
        if(loggerPool[id].logtoserver){
            logToServer(id);
        }
    },
    
    logBenchmark: function(id, timestoIterate, func, debug, log){
        var timeSum = 0;
        for(var x = 0; x < timestoIterate; x++){
            perfLogger.startTimeLogging(id, "benchmarking "+ func, 
    false, false);
            func();
            perfLogger.stopTimeLogging(id)
            timeSum += loggerPool[id].runtime
        }
        loggerPool[id].avgRunTime = timeSum/timestoIterate
        if(debug){
                drawToDebugScreen(id)
        }
        if(log){
                logToServer(id)
        }
    },
    
    //expose derived performance data
    perceivedTime: function(){
        return _pTime;
    }, 
    redirectTime: function(){
        _redirTime;
    }, 
    cacheTime: function(){
        return _cacheTime;
    }, 
    dnsLookupTime: function(){
        return _dnsTime;
    },
    tcpConnectionTime: function(){
        return _tcpTime;
    },
    roundTripTime: function(){
        return _roundtripTime;
    },
    pageRenderTime: function(){
        return _renderTime;
    },
    
    showPerformanceMetrics: function(){
        this.startTimeLogging("no_id", "draw perf data to page"
,true,true);
        this.stopTimeLogging("no_id");
        
    }
    
}
}();

performance.now = (function() {
  return performance.now       ||
         performance.mozNow    ||
         performance.msNow     ||
         performance.oNow      ||
         performance.webkitNow ||
         function() { return new Date().getTime(); };
})();
View Code

 

實現和可視化

要想使用 perfLogger.js 腳本可視化瀏覽器內的性能,能夠將該腳本嵌入頁面中,在頁面的 onload 事件上,您能夠將性能數據推送回端點,將它們保存爲一個平面文件。GitHub 中的 perfLogger 項目附帶了一個 PHP 腳本,名爲 savePerfData.php,該腳本剛好提供了此功能。該文件的源代碼如清單 6 所示。

清單 6. savePerfData.php 的源代碼
<?php
require("util/fileio.php");

$logfile = "log/runtimeperf_results.txt";
$benchmarkResults = formatResults($_POST["data"]);

saveLog($benchmarkResults, $logfile);

function formatResults($r){
    print_r($r);
    $r = stripcslashes($r);
    $r = json_decode($r);
    if(json_last_error() > 0){
        die("invalid json");
    }
    return($r);
}

function formatNewLog($file){
    $headerline = "IP, TestID, StartTime, StopTime, RunTime, 
URL, UserAgent, PerceivedLoadTime, PageRenderTime, RoundTripTime, 
TCPConnectionTime, DNSLookupTime, CacheTime, RedirectTime";
    appendToFile($headerline, $file);
}


function saveLog($obj, $file){
    if(!file_exists($file)){
        formatNewLog($file);
    }
    $obj->useragent = cleanCommas($obj->useragent);
    $newLine = $_SERVER["REMOTE_ADDR"] . "," . $obj->id .","
. $obj->startTime . "," . $obj->stopTime . "," . $obj->runtime . ","
. $obj->url . "," . $obj->useragent . $obj->perceivedTime . ","
. $obj->pageRenderTime . "," . $obj->roundTripTime . ","
. $obj->tcpConnectionTime . "," . $obj->dnsLookupTime . "," 
. $obj->cacheTime . "," . $obj->redirectTime;
    appendToFile($newLine, $file);
}

function cleanCommas($data){
    return implode("", explode(",", $data));
}

?>
View Code

 

這個 PHP 實際上將 perfLogger.js 發送的 POST 數據保存爲一個平面文件,格式如清單 7 所示。

清單 7. perfLogger.js 發送的 POST 數據
IP, TestID, StartTime, StopTime, RunTime, URL, UserAgent, 
PerceivedLoadTime, PageRenderTime, RoundTripTime, TCPConnectionTime, 
DNSLookupTime, CacheTime, RedirectTime
75.149.106.130,page_render,1341243219599,1341243220218,619
,http://www.tom-barker.com/blog/?p=x,Mozilla/5.0 (Macintosh; 
Intel Mac OS X 10.5; rv:13.0) Gecko/20100101 
Firefox/13.0.1790,261,-2,36,0,-4,0

 

此時此刻,您能夠看到有一些很好的數據點值得關注,例如:

  • 用戶代理所用的平均加載時間
  • 在平均加載時間方面,HTTP 事務流程的哪一部分所用的時間最多
  • 整體的加載時間分佈

在 GitHub 存儲庫中,還有一個 R 腳本,名爲 runtimePerformance.R,該腳本將會攝取您生成的這個日誌文件,並實現數據可視化(參見清單 8)。

清單 8. 名爲 runtimePerformance.R 的 R 腳本
dataDirectory <- "/Applications/MAMP/htdocs/lab/log/"
chartDirectory <- "/Applications/MAMP/htdocs/lab/charts/"
testname = "page_render"

perflogs <- read.table(paste(dataDirectory, "runtimeperf
_results.csv", sep=""), header=TRUE, sep=",")
perfchart <- paste(chartDirectory, "runtime_",testname, ".
pdf", sep="")

loadTimeDistrchart <- paste(chartDirectory, 
"loadtime_distribution.pdf", sep="")
requestBreakdown <- paste(chartDirectory, 
"avgtime_inrequest.pdf", sep="")
loadtime_bybrowser <- paste(chartDirectory, 
"loadtime_bybrowser.pdf", sep="")

pagerender <- perflogs[perflogs$TestID == "page_render",]
df <- data.frame(pagerender$UserAgent, pagerender$RunTime)
df <- by(df$pagerender.RunTime, df$pagerender.UserAgent, mean)
df <- df[order(df)]

pdf(perfchart, width=10, height=10)
opar <- par(no.readonly=TRUE)
    par(las=1, mar=c(10,10,10,10))        
    barplot(df, horiz=TRUE)
par(opar)    
dev.off()


getDFByBrowser<-function(data, browsername){
    return(data[grep(browsername, data$UserAgent),])
}


printLoadTimebyBrowser <- function(){
    chrome <- getDFByBrowser(perflogs, "Chrome")
    firefox <- getDFByBrowser(perflogs, "Firefox")
     ie <- getDFByBrowser(perflogs, "MSIE")

    meanTimes <- data.frame(mean(chrome$PerceivedLoadTime),
 mean(firefox$PerceivedLoadTime), mean(ie$PerceivedLoadTime))
    colnames(meanTimes) <- c("Chrome", "Firefox",
"Internet Explorer")
    pdf(loadtime_bybrowser, width=10, height=10)
        barplot(as.matrix(meanTimes), main="Average Perceived Load 
Time\nBy Browser", ylim=c(0, 600), ylab="milliseconds")
    dev.off()
}


pdf(loadTimeDistrchart, width=10, height=10)
    hist(perflogs$PerceivedLoadTime, main="Distribution of 
Perceived 
Load Time", xlab="Perceived Load Time in Milliseconds", 
col=c("#CCCCCC"))
dev.off()

avgTimeBreakdownInRequest <- function(){

#expand exponential notation
options(scipen=100, digits=3)

#set any negative values to 0
perflogs$PageRenderTime[perflogs$PageRenderTime < 0] <- 0
perflogs$RoundTripTime[perflogs$RoundTripTime < 0] <- 0
perflogs$TCPConnectionTime[perflogs$TCPConnectionTime < 0] <- 0
perflogs$DNSLookupTime[perflogs$DNSLookupTime < 0] <- 0

#capture avg times
avgTimes <- data.frame(mean(perflogs$PageRenderTime), 
mean(perflogs$RoundTripTime), mean(perflogs$TCPConnectionTime),
mean(perflogs$DNSLookupTime))
colnames(avgTimes) <- c("PageRenderTime", "RoundTripTime", 
"TCPConnectionTime", "DNSLookupTime")
pdf(requestBreakdown, width=10, height=10)
opar <- par(no.readonly=TRUE)
    par(las=1, mar=c(10,10,10,10))        
    barplot(as.matrix(avgTimes), horiz=TRUE, main="Average Time 
Spent\nDuring HTTP Request", xlab="Milliseconds")
par(opar)    
dev.off()
    
}

printLoadTimebyBrowser()
avgTimeBreakdownInRequest()

 

這個 R 腳本附帶了一些內置的功能,例如 printLoadTimebyBrowser 和 avgTimeBreakdownInRequest。圖 4 是 printLoadTimebyBrowser 輸出的屏幕截圖。

圖 4. printLoadTimebyBrowser 的輸出

輸出

圖 5 是 avgTimeBreakdownInRequest 的屏幕截圖。

圖 5. avgTimeBreakdownInRequest 代碼的輸出code

輸出

在將性能數據加載到 R 會話中以後,全部已攝取的數據指標都存儲在一個名爲 perflogs 的數據幀內,這樣您就能夠訪問單獨的列,如清單 9 所示。

清單 9. 已攝取的指標存儲在名爲 perflogs 的數據幀內
perflogs$PerceivedLoadTime
perflogs$ PageRenderTime
perflogs$RoundTripTime
perflogs$TCPConnectionTime
perflogs$DNSLookupTime
perflogs$UserAgent

 

該代碼支持您開始執行一些探索性的數據分析,好比建立柱狀圖來查看用戶羣的感知加載時間的分佈,如清單 10 所示。

清單 10. 用戶羣的感知加載時間
hist(perflogs$PerceivedLoadTime, main="Distribution of 
Perceived Load Time", xlab="Perceived Load Time in Milliseconds", 
col=c("#CCCCCC"))
dev.off()

 

圖 6 顯示了用戶羣的感知加載時間分佈柱狀圖。

圖 6. 用戶羣的感知加載時間分佈柱狀圖

用戶羣的感知加載時間分佈柱狀圖

結束語

本文幫助您更好地瞭解了 Performance 對象中一些功能,並模擬瞭如何使用可從該對象收集的瀏覽器內指標。經過這裏介紹的模型,您能夠從實際用戶羣中捕獲真正的用戶指標,這是您能夠收集並跟蹤的最有價值的性能指標類型。

若是願意的話,您還可使用該模型中的 perfLogger.js 和全部實用程序文件。您能夠隨時發表您本身的意見以及對該項目的更改。



源文連接

相關文章
相關標籤/搜索