1# Copyright 2022 The Chromium Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4from __future__ import annotations 5 6import logging 7from typing import TYPE_CHECKING 8 9from crossbench.probes import metric 10from crossbench.probes.json import JsonResultProbe 11from crossbench.probes.probe import ProbeIncompatibleBrowser 12 13if TYPE_CHECKING: 14 from crossbench.browsers.browser import Browser 15 from crossbench.env import HostEnvironment 16 from crossbench.probes.results import ProbeResult 17 from crossbench.runner.actions import Actions 18 from crossbench.runner.groups.browsers import BrowsersRunGroup 19 from crossbench.runner.groups.stories import StoriesRunGroup 20 from crossbench.types import Json 21 22 23class PerformanceEntriesProbe(JsonResultProbe): 24 """ 25 Extract all JavaScript PerformanceEntry [1] from a website. 26 Website owners can define more entries via `performance.mark()`. 27 28 [1] https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry 29 """ 30 NAME = "performance.entries" 31 32 def validate_browser(self, env: HostEnvironment, browser: Browser) -> None: 33 super().validate_browser(env, browser) 34 if not hasattr(browser, "js"): 35 raise ProbeIncompatibleBrowser(self, browser, 36 "Needs browser with JS-execution support") 37 38 def to_json(self, actions: Actions) -> Json: 39 return actions.js(""" 40 let data = { __proto__: null, paint: {}, mark: {} }; 41 for (let entryType of Object.keys(data)) { 42 for (let entry of performance.getEntriesByType(entryType)) { 43 const typeData = data[entryType]; 44 let values = typeData[entry.name]; 45 if (values === undefined) { 46 values = typeData[entry.name] = { startTime: [], duration: [] }; 47 } 48 for (let metricName of Object.keys(values)) { 49 values[metricName].push(entry[metricName]); 50 } 51 } 52 } 53 data.navigation = {}; 54 const navigationTiming = performance.getEntriesByType("navigation")[0]; 55 for (let name in navigationTiming) { 56 const value = navigationTiming[name]; 57 if (typeof value !== "number") continue; 58 data.navigation[name] = value; 59 } 60 return data; 61 """) 62 63 def merge_stories(self, group: StoriesRunGroup) -> ProbeResult: 64 stories = list(group.stories) 65 if len(stories) > 1: 66 logging.warning( 67 "%s: Merging performance.entries from %d possibly unrelated pages %s", 68 group.browser.unique_name, len(stories), 69 ", ".join(story.name for story in stories)) 70 merged = metric.MetricsMerger.merge_json_list( 71 (story_group.results[self].json 72 for story_group in group.repetitions_groups), 73 merge_duplicate_paths=True) 74 return self.write_group_result(group, merged) 75 76 def merge_browsers(self, group: BrowsersRunGroup) -> ProbeResult: 77 # TODO: recreate the CSV from the merged JSON files since we might not 78 # get the same values in all browsers. 79 # TODO: Add merged browser data with separate stories. 80 return self.merge_browsers_json_list(group).merge( 81 self.merge_browsers_csv_list(group)) 82