Android端末のNavigation TimingをPlaywrightでPCから自動取得 (Node.js)
はじめに
Android端末のNavigation Timing APIを使って、ネットワークパフォーマンスを測定してみようと思います。
ChromeのRemote Debuggingを使えばAndroid端末のNavigation Timing取得は可能ですが、可能な限り自動化したいと思いPlaywrightを使ってPCからコマンド実行で取得することをめざします。
ChromeのRemote Debugging
Android端末の操作はまだNode.jsのみの対応だったので、Node.jsで試してみます。
サンプルコード
早速ですがサンプルコードです。
(ES Modulesをベースに書いてみたのでmjsの拡張子にしていますが、お好みで)
import { _android } from 'playwright-core'; // playwright-coreモジュールのインポート
import { promises as fs } from 'fs'; // fsモジュールのインポート
import { format } from 'date-fns'; // date-fnsモジュールのインポート
(async() => {
// 定数定義
const TARGET_URL = 'https://developer.chrome.com'; // 測定対象URL
const DATETIME_FORMAT = 'yyyyMMdd-HHmmss'; // 出力時の日時フォーマット
// 端末一覧を取得
const deviceList = await _android.devices();
if(deviceList.length === 0) {
// デバイス未接続の場合
console.log('No Android devices found.');
return;
} else {
// 端末接続ありの場合
// 保存ファイル名を生成
const regrex = /https?:\/\//g;
const prefix = TARGET_URL.replaceAll(regrex,'').replaceAll('/','_'); // TARGET_URLからプレフィクス生成
const datetime = format(new Date(), DATETIME_FORMAT); // 実行時刻をフォーマット
const filename = prefix + '_' + datetime;
// ToDo: 複数端末接続時の挙動
// 1台目の端末に接続
let device = deviceList[0];
// 端末のモデル名とシリアル番号を取得
let deviceName = await device.model() + '_' + device.serial();
console.log('Android device ' + deviceName + ' connected.');
// ブラウザの立ち上げ及び測定対象URLへの接続
let context = await device.launchBrowser();
let page = await context.newPage();
await page.goto(TARGET_URL);
// 測定対象URLのNavigation Timingを取得
let navigationTiming = await page.evaluate(() =>
JSON.stringify(performance.getEntriesByType('navigation'))
);
// Navigation Timingを出力(参考)
// console.log(navigationTiming);
// 何かに使えるかもしれないのでAndroidのTelephony関連のダンプを出力
let telephonyInfo = device.shell('dumpsys telephony.registry');
await fs.writeFile(deviceName + '_' + filename + '_Telephony.txt', (await telephonyInfo).toString());
// 何かに使えるかもしれないのでAndroidのLocation関連のダンプを出力
let locationInfo = device.shell('dumpsys location');
await fs.writeFile(deviceName + '_' + filename + '_Location.txt', (await locationInfo).toString());
// Navigation TimingのJSONとスクリーンショットを出力
await fs.writeFile(deviceName + '_' + filename + '_NavigationTiming.json', navigationTiming);
await device.screenshot({ path: deviceName + '_' + filename + '_ScreenShot.png' });
// 開いたタブを閉じる
await page.close();
// 端末とのコネクションを切断
await context.close();
await device.close();
}
})();
実行結果
端末とPCをUSB接続の上、nodeコマンドで実行します。
% node androidNavigationTiming.mjs
4つのファイルが生成されます。
- Navigation Timingファイル(.json)
- Navigation Timingを記録したJSONファイル
- スクリーンショット(.png)
- 実行時のスクリーンショット (上手く撮れないこともある)
- Telephony情報(.txt)
- Android側のTelephony.registryのdumpsys (ネットワークの状態に関する情報を取得したかったため)
- Location情報(.txt)
- Android側のLocationのdumpsys (端末の位置情報を取得したかったため)
生成ファイル
Navigation Timingファイル(.json)
いわゆるTime to First Byteでよく見る図の、startTime
からloadEventEnd
までのタイミングが出力されます。
出力結果
[
{
"name": "https://developer.chrome.com/?hl=ja",
"entryType": "navigation",
"startTime": 0,
"duration": 2305.7000000029802,
"initiatorType": "navigation",
"deliveryType": "",
"nextHopProtocol": "h3",
"renderBlockingStatus": "non-blocking",
"workerStart": 752.4000000059605,
"redirectStart": 2.800000011920929,
"redirectEnd": 749.1000000089407,
"fetchStart": 756,
"domainLookupStart": 756,
"domainLookupEnd": 756,
"connectStart": 756,
"secureConnectionStart": 756,
"connectEnd": 756,
"requestStart": 756,
"responseStart": 1675.2000000029802,
"firstInterimResponseStart": 1675.2000000029802,
"finalResponseHeadersStart": 0,
"responseEnd": 1797,
"transferSize": 142508,
"encodedBodySize": 142208,
"decodedBodySize": 142208,
"responseStatus": 200,
"serverTiming": [],
"unloadEventStart": 0,
"unloadEventEnd": 0,
"domInteractive": 1975.2000000029802,
"domContentLoadedEventStart": 1975.2000000029802,
"domContentLoadedEventEnd": 1975.300000011921,
"domComplete": 2305.7000000029802,
"loadEventStart": 2305.7000000029802,
"loadEventEnd": 2305.7000000029802,
"type": "navigate",
"redirectCount": 1,
"activationStart": 0,
"criticalCHRestart": 0,
"notRestoredReasons": null
}
]
スクリーンショット(.png)
実行時点でのデバイスのスクリーンショットを取得して保存します。
タイミングによって上手く撮れない (タブを閉じた後に記録される) ことがあります。
Telephony情報(.txt)
AndroidのTelephony.registryのdumpsysをそのまま出力しています。
接続しているセルラーネットワーク情報が出力されます。
見づらいデータですが、オフラインで集計するときに何か役に立つかもしれません。
(結果を一部マスクの上で抜粋しています。2147483647
が頻出しますが、31bitの最大値に等しいことから無効値のように見えます)
Telephony情報 出力結果
mCellIdentity=CellIdentityLte:{ mCi=******44 mPci=**6 mTac=***1 mEarfcn=276 mBands=[1] mBandwidth=15000 mMcc=440 mMnc=10 mAlphaLong=docomo mAlphaShort=docomo mAdditionalPlmns={} mCsgInfo=null}
mCellInfo=[CellInfoLte:{mRegistered=YES mTimeStamp=1974541987150774ns mCellConnectionStatus=0 CellIdentityLte:{ mCi=******44 mPci=**6 mTac=***1 mEarfcn=276 mBands=[1] mBandwidth=15000 mMcc=440 mMnc=10 mAlphaLong=docomo mAlphaShort=docomo mAdditionalPlmns={} mCsgInfo=null} CellSignalStrengthLte: rssi=-69 rsrp=-95 rsrq=-6 rssnr=2147483647 cqiTableIndex=2147483647 cqi=2147483647 ta=1 level=3 parametersUseForLevel=0 android.telephony.CellConfigLte :{ isEndcAvailable = false }}, CellInfoLte:{mRegistered=NO mTimeStamp=1974541987150774ns mCellConnectionStatus=0 CellIdentityLte:{ mCi=********47 mPci=**6 mTac=********47 mEarfcn=42491 mBands=[42] mBandwidth=2147483647 mMcc=null mMnc=null mAlphaLong= mAlphaShort= mAdditionalPlmns={} mCsgInfo=null} CellSignalStrengthLte: rssi=-85 rsrp=-103 rsrq=-9 rssnr=2147483647 cqiTableIndex=2147483647 cqi=2147483647 ta=2147483647 level=2 parametersUseForLevel=0 android.telephony.CellConfigLte :{ isEndcAvailable = false }}, CellInfoLte:{mRegistered=NO mTimeStamp=1974541987150774ns mCellConnectionStatus=0 CellIdentityLte:{ mCi=********47 mPci=**1 mTac=********47 mEarfcn=42491 mBands=[42] mBandwidth=2147483647 mMcc=null mMnc=null mAlphaLong= mAlphaShort= mAdditionalPlmns={} mCsgInfo=null} CellSignalStrengthLte: rssi=-85 rsrp=-109 rsrq=-15 rssnr=2147483647 cqiTableIndex=2147483647 cqi=2147483647 ta=2147483647 level=1 parametersUseForLevel=0 android.telephony.CellConfigLte :{ isEndcAvailable = false }}, CellInfoLte:{mRegistered=NO mTimeStamp=1974541987150774ns mCellConnectionStatus=0 CellIdentityLte:{ mCi=********47 mPci=**7 mTac=********47 mEarfcn=42689 mBands=[42] mBandwidth=2147483647 mMcc=null mMnc=null mAlphaLong= mAlphaShort= mAdditionalPlmns={} mCsgInfo=null} CellSignalStrengthLte: rssi=-85 rsrp=-105 rsrq=-9 rssnr=2147483647 cqiTableIndex=2147483647 cqi=2147483647 ta=2147483647 level=2 parametersUseForLevel=0 android.telephony.CellConfigLte :{ isEndcAvailable = false }}, CellInfoLte:{mRegistered=NO mTimeStamp=1974541987150774ns mCellConnectionStatus=0 CellIdentityLte:{ mCi=********47 mPci=**2 mTac=********47 mEarfcn=42689 mBands=[42] mBandwidth=2147483647 mMcc=null mMnc=null mAlphaLong= mAlphaShort= mAdditionalPlmns={} mCsgInfo=null} CellSignalStrengthLte: rssi=-85 rsrp=-108 rsrq=-12 rssnr=2147483647 cqiTableIndex=2147483647 cqi=2147483647 ta=2147483647 level=1 parametersUseForLevel=0 android.telephony.CellConfigLte :{ isEndcAvailable = false }}, CellInfoLte:{mRegistered=NO mTimeStamp=1974541987150774ns mCellConnectionStatus=0 CellIdentityLte:{ mCi=********47 mPci=**7 mTac=********47 mEarfcn=42689 mBands=[42] mBandwidth=2147483647 mMcc=null mMnc=null mAlphaLong= mAlphaShort= mAdditionalPlmns={} mCsgInfo=null} CellSignalStrengthLte: rssi=-85 rsrp=-112 rsrq=-16 rssnr=2147483647 cqiTableIndex=2147483647 cqi=2147483647 ta=2147483647 level=1 parametersUseForLevel=0 android.telephony.CellConfigLte :{ isEndcAvailable = false }}, CellInfoLte:{mRegistered=NO mTimeStamp=1974541987150774ns mCellConnectionStatus=0 CellIdentityLte:{ mCi=********47 mPci=**8 mTac=********47 mEarfcn=42689 mBands=[42] mBandwidth=2147483647 mMcc=null mMnc=null mAlphaLong= mAlphaShort= mAdditionalPlmns={} mCsgInfo=null} CellSignalStrengthLte: rssi=-85 rsrp=-115 rsrq=-18 rssnr=2147483647 cqiTableIndex=2147483647 cqi=2147483647 ta=2147483647 level=1 parametersUseForLevel=0 android.telephony.CellConfigLte :{ isEndcAvailable = false }}]
{
"mCellIdentity": {
"mCi": "******44",
"mPci": "**6",
"mTac": "***1",
"mEarfcn": 276,
"mBands": [1],
"mBandwidth": 15000,
"mMcc": 440,
"mMnc": 10,
"mAlphaLong": "docomo",
"mAlphaShort": "docomo",
"mAdditionalPlmns": {},
"mCsgInfo": null
},
"mCellInfo": [
{
"mRegistered": "YES",
"mTimeStamp": "1974541987150774ns",
"mCellConnectionStatus": 0,
"CellIdentityLte": {
"mCi": "******44",
"mPci": "**6",
"mTac": "***1",
"mEarfcn": 276,
"mBands": [1],
"mBandwidth": 15000,
"mMcc": 440,
"mMnc": 10,
"mAlphaLong": "docomo",
"mAlphaShort": "docomo",
"mAdditionalPlmns": {},
"mCsgInfo": null
},
"CellSignalStrengthLte": {
"rssi": -69,
"rsrp": -95,
"rsrq": -6,
"rssnr": 2147483647,
"cqiTableIndex": 2147483647,
"cqi": 2147483647,
"ta": 1,
"level": 3,
"parametersUseForLevel": 0
},
"android.telephony.CellConfigLte": {
"isEndcAvailable": false
}
},
{
"mRegistered": "NO",
"mTimeStamp": "1974541987150774ns",
"mCellConnectionStatus": 0,
"CellIdentityLte": {
"mCi": "********47",
"mPci": "**6",
"mTac": "********47",
"mEarfcn": 42491,
"mBands": [42],
"mBandwidth": 2147483647,
"mMcc": null,
"mMnc": null,
"mAlphaLong": "",
"mAlphaShort": "",
"mAdditionalPlmns": {},
"mCsgInfo": null
},
"CellSignalStrengthLte": {
"rssi": -85,
"rsrp": -103,
"rsrq": -9,
"rssnr": 2147483647,
"cqiTableIndex": 2147483647,
"cqi": 2147483647,
"ta": 2147483647,
"level": 2,
"parametersUseForLevel": 0
},
"android.telephony.CellConfigLte": {
"isEndcAvailable": false
}
},
{
"mRegistered": "NO",
"mTimeStamp": "1974541987150774ns",
"mCellConnectionStatus": 0,
"CellIdentityLte": {
"mCi": "********47",
"mPci": "**1",
"mTac": "********47",
"mEarfcn": 42491,
"mBands": [42],
"mBandwidth": 2147483647,
"mMcc": null,
"mMnc": null,
"mAlphaLong": "",
"mAlphaShort": "",
"mAdditionalPlmns": {},
"mCsgInfo": null
},
"CellSignalStrengthLte": {
"rssi": -85,
"rsrp": -109,
"rsrq": -15,
"rssnr": 2147483647,
"cqiTableIndex": 2147483647,
"cqi": 2147483647,
"ta": 2147483647,
"level": 1,
"parametersUseForLevel": 0
},
"android.telephony.CellConfigLte": {
"isEndcAvailable": false
}
}
]
}
Location情報(.txt)
AndroidのLocationのdumpsysをそのまま出力しています。
端末の位置情報(緯度経度)が出力されるので、オフラインで解析するときなどに役に立つかもしれません。
Location情報 出力結果
last location=Location[gps 35.******,139.****** hAcc=15.286787 et=+22d22h4m3s206ms alt=118.6220703125 vAcc=9.578282 vel=0.0 sAcc=0.44611505 {Bundle[{satellites=15, maxCn0=38, meanCn0=24}]}]
last location=Location[network 35.******,139.****** hAcc=13.0 et=+22d22h4m2s894ms alt=57.099998474121094 vAcc=7.719068 {Bundle[{verticalAccuracy=7.719068}]}]
環境の準備
OSと端末
- macOS Sequioia 15.3.1
- Xperia 1 IV (Android 14)
OSはmacOSで試しましたが、他のOSでも順次試してみる予定です。
Node.js
今回はmacOSだったので、homebrewからインストール。
手順などは以下のように色々なサイトにあると思いますので割愛します。
Windowsだと以下のような手順になるかと思います。
必要なnpmモジュールのインストール
以下のモジュールをインストールします。
npm install @types/node fs date-fns playwright-core
-
@types/node
- fsモジュールを使うときにエラーになるため、定義モジュールをインストールしておく
-
fs
- JSONファイルをPC側に書き込むためのモジュール
-
date-fns
- 日付・時刻のフォーマットのためのモジュール
-
playwright-core
- Android端末のChrome操作自動化に必要なPlaywrightのコアモジュール
インストール結果はこんな感じになりました。
% npm list
hoge@ /Users/hoge
├── @types/node@22.13.10
├── date-fns@4.1.0
├── fs@0.0.1-security
└── playwright-core@1.51.0
Playwright
MicrosoftがリリースしているE2Eテストやブラウザ自動化が可能なツール。
Getting Started > Installation
に従ってインストール。
adb
Android SDK platform toolsからダウンロードして展開。
実行パスを通すなどはお好みで良いかと思いますが、私は通しておきました。
$ export PATH=$PATH:~/platform-tools/
Android Studioだと、パスなどは気にせずadbもインストールされるかもしれません。
VSCode
エディタならなんでも良いですが、TerminalでのNode.jsの実行も楽だったので。
端末
USBでPCと接続の上、adbで制御できるように設定します。
ちょうど同じ内容に触れられている記事があったのでそちらを参照ください。
設定手順のスクリーンショットは参考までに貼っておきます。
「接続中は画面をオフにしない」のトグルもONにしておいた方が良いと思います。画面OFFの場合、今回試した制御は待ち状態になってしまって完了しません。
最後にPCに接続した際に、端末側で「この端末からのUSBデバッグを許可しますか」のダイアログが出ますので、忘れずに許可してください。
(参考) 設定手順のスクリーンショット
Developer Options
USB DebuggingのON
常時画面ON
ChromeのCommand Line制御有効化
VSCodeでの実行
サンプルコードをVSCodeに貼り付けて保存の上で、VSCodeのターミナルからnode
コマンドを実行すると、mjsファイルと同じディレクトリに 結果が出力 されます。
% node androidNavigationTiming.mjs
VSCodeでの実行
自動化
端末と接続した状態で上記のスクリプトを定期的に実行させれば、自動実行可能です。
macOSならcronかAutomator、Windowsならタスクスケジューラを使えば実行可能かと思います。
おわりに
最初はPythonで作ろうと思ったのですが、Android対応はまだNode.js限定のようですね。
まだToDoとして
- 複数端末が接続されている場合にどうするのか
- Chromeのタブが上手くクローズされない (どんどん増えていく)
あたりの課題はあるので、ぼちぼち考えてみようかなと思います。
Discussion