browsertime 16.3.0 → 16.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/browserscripts/pageinfo/interactionToNextPaintInfo.js +117 -0
- package/browserscripts/pageinfo/visualElements.js +2 -1
- package/browserscripts/timings/interactionToNextPaint.js +116 -0
- package/browsertime/visualmetrics-portable.py +16 -11
- package/lib/chrome/longTaskMetrics.js +5 -0
- package/lib/support/cli.js +6 -0
- package/lib/video/postprocessing/finetune/getTimingMetrics.js +7 -1
- package/lib/video/postprocessing/visualmetrics/visualMetrics.js +5 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Browsertime changelog (we do [semantic versioning](https://semver.org))
|
|
2
2
|
|
|
3
|
+
## 16.6.0 - 2022-05-20
|
|
4
|
+
### Added
|
|
5
|
+
* Implemented experimental Interaction to next paint that's useful if you test user journeys [#1791](https://github.com/sitespeedio/browsertime/pull/1791).
|
|
6
|
+
* Track when the last CPU long task happen as explained by Andy Davies of the webperf Slack channel [#1789](https://github.com/sitespeedio/browsertime/pull/1789).
|
|
7
|
+
|
|
8
|
+
## 16.5.0 - 2022-05-11
|
|
9
|
+
### Added
|
|
10
|
+
* Make it possible to configure the max size in pixels for the filmstrip screenshots using `--videoParams.thumbsize` [#1787](https://github.com/sitespeedio/browsertime/pull/1787).
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
* Handle all boundaries when cropping image in portable script, thank you [Gregory Mierzwinski](https://github.com/gmierz) for PR [#1788](https://github.com/sitespeedio/browsertime/pull/1788).
|
|
14
|
+
## 16.4.0 - 2022-05-11
|
|
15
|
+
### Added
|
|
16
|
+
* If we have the Largest Contentful Paint for the video, add that to the video text instead of DOMContentLoaded [#1783](https://github.com/sitespeedio/browsertime/pull/1783).
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
* Only get LCP in Visual Metrics if the LCP API gives us a element [#1786](https://github.com/sitespeedio/browsertime/pull/1786).
|
|
3
20
|
## 16.3.0 - 2022-05-07
|
|
4
21
|
### Added
|
|
5
22
|
* If you use `--visualElements` and the browser supports the Largest Contentful API, we also record
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
// This is an updated version of
|
|
3
|
+
// https://github.com/GoogleChrome/web-vitals/blob/next/src/onINP.ts
|
|
4
|
+
|
|
5
|
+
const supported = PerformanceObserver.supportedEntryTypes;
|
|
6
|
+
if (!supported || supported.indexOf('event') === -1) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const observer = new PerformanceObserver(list => {});
|
|
11
|
+
// Event Timing entries have their durations rounded to the nearest 8ms,
|
|
12
|
+
// so a duration of 40ms would be any event that spans 2.5 or more frames
|
|
13
|
+
// at 60Hz. This threshold is chosen to strike a balance between usefulness
|
|
14
|
+
// and performance. Running this callback for any interaction that spans
|
|
15
|
+
// just one or two frames is likely not worth the insight that could be
|
|
16
|
+
// gained.
|
|
17
|
+
observer.observe({type: 'event', buffered: true, durationThreshold: 40});
|
|
18
|
+
const entries = observer.takeRecords();
|
|
19
|
+
|
|
20
|
+
// To prevent unnecessary memory usage on pages with lots of interactions,
|
|
21
|
+
// store at most 10 of the longest interactions to consider as INP candidates.
|
|
22
|
+
const MAX_INTERACTIONS_TO_CONSIDER = 10;
|
|
23
|
+
|
|
24
|
+
// A list of longest interactions on the page (by latency) sorted so the
|
|
25
|
+
// longest one is first. The list is as most MAX_INTERACTIONS_TO_CONSIDER long.
|
|
26
|
+
let longestInteractionList = [];
|
|
27
|
+
|
|
28
|
+
// A mapping of longest interactions by their interaction ID.
|
|
29
|
+
// This is used for faster lookup.
|
|
30
|
+
const longestInteractionMap = {};
|
|
31
|
+
|
|
32
|
+
const getInteractionCountForNavigation = () => {
|
|
33
|
+
// I guess the interactionCount is coming in Chrome later on
|
|
34
|
+
if (performance.interactionCount) {
|
|
35
|
+
return performance.interactionCount;
|
|
36
|
+
} else {
|
|
37
|
+
const observerForAll = new PerformanceObserver(list => {});
|
|
38
|
+
observerForAll.observe({
|
|
39
|
+
type: 'event',
|
|
40
|
+
buffered: true,
|
|
41
|
+
durationThreshold: 0
|
|
42
|
+
});
|
|
43
|
+
const allEntries = observerForAll.takeRecords();
|
|
44
|
+
let interactionCountEstimate = 0;
|
|
45
|
+
let minKnownInteractionId = Infinity;
|
|
46
|
+
let maxKnownInteractionId = 0;
|
|
47
|
+
|
|
48
|
+
for (let e of allEntries) {
|
|
49
|
+
if (e.interactionId) {
|
|
50
|
+
minKnownInteractionId = Math.min(
|
|
51
|
+
minKnownInteractionId,
|
|
52
|
+
e.interactionId
|
|
53
|
+
);
|
|
54
|
+
maxKnownInteractionId = Math.max(
|
|
55
|
+
maxKnownInteractionId,
|
|
56
|
+
e.interactionId
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
interactionCountEstimate = maxKnownInteractionId
|
|
60
|
+
? (maxKnownInteractionId - minKnownInteractionId) / 7 + 1
|
|
61
|
+
: 0;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return interactionCountEstimate;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const estimateP98LongestInteraction = () => {
|
|
69
|
+
const candidateInteractionIndex = Math.min(
|
|
70
|
+
longestInteractionList.length - 1,
|
|
71
|
+
Math.floor(getInteractionCountForNavigation() / 50)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return longestInteractionList[candidateInteractionIndex];
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (entries.length > 0) {
|
|
78
|
+
for (let entry of entries) {
|
|
79
|
+
const minLongestInteraction =
|
|
80
|
+
longestInteractionList[longestInteractionList.length - 1];
|
|
81
|
+
const existingInteraction = longestInteractionMap[entry.interactionId];
|
|
82
|
+
if (
|
|
83
|
+
existingInteraction ||
|
|
84
|
+
longestInteractionList.length < MAX_INTERACTIONS_TO_CONSIDER ||
|
|
85
|
+
entry.duration > minLongestInteraction.latency
|
|
86
|
+
) {
|
|
87
|
+
// If the interaction already exists, update it. Otherwise create one.
|
|
88
|
+
if (existingInteraction) {
|
|
89
|
+
existingInteraction.entries.push(entry);
|
|
90
|
+
existingInteraction.latency = Math.max(
|
|
91
|
+
existingInteraction.latency,
|
|
92
|
+
entry.duration
|
|
93
|
+
);
|
|
94
|
+
} else {
|
|
95
|
+
const interaction = {
|
|
96
|
+
id: entry.interactionId,
|
|
97
|
+
latency: entry.duration,
|
|
98
|
+
entries: [entry]
|
|
99
|
+
};
|
|
100
|
+
longestInteractionMap[interaction.id] = interaction;
|
|
101
|
+
longestInteractionList.push(interaction);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Sort the entries by latency (descending) and keep only the top ten.
|
|
105
|
+
longestInteractionList.sort((a, b) => b.latency - a.latency);
|
|
106
|
+
longestInteractionList
|
|
107
|
+
.splice(MAX_INTERACTIONS_TO_CONSIDER)
|
|
108
|
+
.forEach(i => {
|
|
109
|
+
delete longestInteractionMap[i.id];
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return estimateP98LongestInteraction();
|
|
114
|
+
} else {
|
|
115
|
+
// nothing to report
|
|
116
|
+
}
|
|
117
|
+
})();
|
|
@@ -133,7 +133,8 @@
|
|
|
133
133
|
const entries = observer.takeRecords();
|
|
134
134
|
if (entries.length > 0) {
|
|
135
135
|
const largestEntry = entries[entries.length - 1];
|
|
136
|
-
|
|
136
|
+
// It seems that the API do not alwayas have a elememt
|
|
137
|
+
if (largestEntry.element && isElementPartlyInViewportAndVisible(largestEntry.element)) {
|
|
137
138
|
keepLargestElementByType('LargestContentfulPaint', largestEntry.element);
|
|
138
139
|
}
|
|
139
140
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
// This is an updated version of
|
|
3
|
+
// https://github.com/GoogleChrome/web-vitals/blob/next/src/onINP.ts
|
|
4
|
+
|
|
5
|
+
const supported = PerformanceObserver.supportedEntryTypes;
|
|
6
|
+
if (!supported || supported.indexOf('event') === -1) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// To prevent unnecessary memory usage on pages with lots of interactions,
|
|
11
|
+
// store at most 10 of the longest interactions to consider as INP candidates.
|
|
12
|
+
const MAX_INTERACTIONS_TO_CONSIDER = 10;
|
|
13
|
+
|
|
14
|
+
// A list of longest interactions on the page (by latency) sorted so the
|
|
15
|
+
// longest one is first. The list is as most MAX_INTERACTIONS_TO_CONSIDER long.
|
|
16
|
+
let longestInteractionList = [];
|
|
17
|
+
|
|
18
|
+
// A mapping of longest interactions by their interaction ID.
|
|
19
|
+
// This is used for faster lookup.
|
|
20
|
+
const longestInteractionMap = {};
|
|
21
|
+
|
|
22
|
+
const getInteractionCountForNavigation = () => {
|
|
23
|
+
// I guess the interactionCount is coming in Chrome later on
|
|
24
|
+
if (performance.interactionCount) {
|
|
25
|
+
return performance.interactionCount;
|
|
26
|
+
} else {
|
|
27
|
+
const observerForAll = new PerformanceObserver(list => {});
|
|
28
|
+
observerForAll.observe({
|
|
29
|
+
type: 'event',
|
|
30
|
+
buffered: true,
|
|
31
|
+
durationThreshold: 0
|
|
32
|
+
});
|
|
33
|
+
const allEntries = observerForAll.takeRecords();
|
|
34
|
+
let interactionCountEstimate = 0;
|
|
35
|
+
let minKnownInteractionId = Infinity;
|
|
36
|
+
let maxKnownInteractionId = 0;
|
|
37
|
+
|
|
38
|
+
for (let e of allEntries) {
|
|
39
|
+
if (e.interactionId) {
|
|
40
|
+
minKnownInteractionId = Math.min(
|
|
41
|
+
minKnownInteractionId,
|
|
42
|
+
e.interactionId
|
|
43
|
+
);
|
|
44
|
+
maxKnownInteractionId = Math.max(
|
|
45
|
+
maxKnownInteractionId,
|
|
46
|
+
e.interactionId
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
interactionCountEstimate = maxKnownInteractionId
|
|
50
|
+
? (maxKnownInteractionId - minKnownInteractionId) / 7 + 1
|
|
51
|
+
: 0;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return interactionCountEstimate;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const estimateP98LongestInteraction = () => {
|
|
59
|
+
const candidateInteractionIndex = Math.min(
|
|
60
|
+
longestInteractionList.length - 1,
|
|
61
|
+
Math.floor(getInteractionCountForNavigation() / 50)
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return longestInteractionList[candidateInteractionIndex];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const observer = new PerformanceObserver(list => {});
|
|
68
|
+
// Event Timing entries have their durations rounded to the nearest 8ms,
|
|
69
|
+
// so a duration of 40ms would be any event that spans 2.5 or more frames
|
|
70
|
+
// at 60Hz. This threshold is chosen to strike a balance between usefulness
|
|
71
|
+
// and performance. Running this callback for any interaction that spans
|
|
72
|
+
// just one or two frames is likely not worth the insight that could be
|
|
73
|
+
// gained.
|
|
74
|
+
observer.observe({type: 'event', buffered: true, durationThreshold: 40});
|
|
75
|
+
const entries = observer.takeRecords();
|
|
76
|
+
|
|
77
|
+
if (entries.length > 0) {
|
|
78
|
+
for (let entry of entries) {
|
|
79
|
+
const minLongestInteraction =
|
|
80
|
+
longestInteractionList[longestInteractionList.length - 1];
|
|
81
|
+
const existingInteraction = longestInteractionMap[entry.interactionId];
|
|
82
|
+
if (
|
|
83
|
+
existingInteraction ||
|
|
84
|
+
longestInteractionList.length < MAX_INTERACTIONS_TO_CONSIDER ||
|
|
85
|
+
entry.duration > minLongestInteraction.latency
|
|
86
|
+
) {
|
|
87
|
+
// If the interaction already exists, update it. Otherwise create one.
|
|
88
|
+
if (existingInteraction) {
|
|
89
|
+
existingInteraction.latency = Math.max(
|
|
90
|
+
existingInteraction.latency,
|
|
91
|
+
entry.duration
|
|
92
|
+
);
|
|
93
|
+
} else {
|
|
94
|
+
const interaction = {
|
|
95
|
+
id: entry.interactionId,
|
|
96
|
+
latency: entry.duration
|
|
97
|
+
};
|
|
98
|
+
longestInteractionMap[interaction.id] = interaction;
|
|
99
|
+
longestInteractionList.push(interaction);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Sort the entries by latency (descending) and keep only the top ten.
|
|
103
|
+
longestInteractionList.sort((a, b) => b.latency - a.latency);
|
|
104
|
+
longestInteractionList
|
|
105
|
+
.splice(MAX_INTERACTIONS_TO_CONSIDER)
|
|
106
|
+
.forEach(i => {
|
|
107
|
+
delete longestInteractionMap[i.id];
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const inp = estimateP98LongestInteraction();
|
|
112
|
+
return inp.latency;
|
|
113
|
+
} else {
|
|
114
|
+
// nothing to report
|
|
115
|
+
}
|
|
116
|
+
})();
|
|
@@ -105,17 +105,22 @@ def crop_im(img, crop_x, crop_y, crop_x_offset, crop_y_offset, gravity=None):
|
|
|
105
105
|
base_x -= crop_x // 2
|
|
106
106
|
base_y -= crop_y // 2
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
108
|
+
# Handle the boundaries of the crop using max to prevent
|
|
109
|
+
# negatives, and min to prevent going over the othersde of
|
|
110
|
+
# the image
|
|
111
|
+
start_x = min(width - 1, max(base_x + crop_x_offset, 0))
|
|
112
|
+
start_y = min(height - 1, max(base_y + crop_y_offset, 0))
|
|
113
|
+
|
|
114
|
+
end_x = min(width - 1, max(start_x + crop_x, 0))
|
|
115
|
+
end_y = min(height - 1, max(start_y + crop_y, 0))
|
|
116
|
+
|
|
117
|
+
if len(img[start_y:end_y, start_x:end_x, :]) == 0:
|
|
118
|
+
raise Exception(
|
|
119
|
+
f"Cropped image is empty. Image dimensions: {img.shape}, "
|
|
120
|
+
f"Crop Region: {crop_x}, {crop_y}, {crop_x_offset}, {crop_y_offset}"
|
|
121
|
+
)
|
|
115
122
|
|
|
116
|
-
return Image.fromarray(
|
|
117
|
-
img[base_y : base_y + crop_y, base_x : base_x + crop_x, :]
|
|
118
|
-
)
|
|
123
|
+
return Image.fromarray(img[start_y:end_y, start_x:end_x, :])
|
|
119
124
|
except BaseException as e:
|
|
120
125
|
logging.exception(e)
|
|
121
126
|
return None
|
|
@@ -2214,7 +2219,7 @@ def calculate_hero_time(progress, directory, hero, viewport):
|
|
|
2214
2219
|
|
|
2215
2220
|
logging.debug(
|
|
2216
2221
|
'Calculating render time for hero element "%s" at position [%d, %d, %d, %d]'
|
|
2217
|
-
% (hero["name"],
|
|
2222
|
+
% (hero["name"], hero_x, hero_y, hero_width, hero_height)
|
|
2218
2223
|
)
|
|
2219
2224
|
|
|
2220
2225
|
# Apply the mask to the target frame to create the reference frame
|
|
@@ -27,9 +27,13 @@ module.exports = function (result, options) {
|
|
|
27
27
|
let longTasksBeforeFirstPaint = 0;
|
|
28
28
|
let longTasksBeforeFirstContentfulPaint = 0;
|
|
29
29
|
let longTasksAfterLoadEventEnd = 0;
|
|
30
|
+
let lastLongTask = 0;
|
|
30
31
|
|
|
31
32
|
for (let longTask of result.browserScripts.pageinfo.longTask) {
|
|
32
33
|
totalDuration += longTask.duration;
|
|
34
|
+
if (longTask.startTime > lastLongTask) {
|
|
35
|
+
lastLongTask = longTask.startTime;
|
|
36
|
+
}
|
|
33
37
|
if (firstPaint && longTask.startTime < firstPaint) {
|
|
34
38
|
longTasksBeforeFirstPaint++;
|
|
35
39
|
totalDurationFirstPaint += longTask.duration;
|
|
@@ -61,6 +65,7 @@ module.exports = function (result, options) {
|
|
|
61
65
|
totalDuration,
|
|
62
66
|
totalBlockingTime: Number(totalBlockingTime.toFixed(0)),
|
|
63
67
|
maxPotentialFid: Number(maxPotentialFid.toFixed(0)),
|
|
68
|
+
lastLongTask: Number(lastLongTask.toFixed(0)),
|
|
64
69
|
beforeFirstPaint: {
|
|
65
70
|
tasks: longTasksBeforeFirstPaint,
|
|
66
71
|
totalDuration: totalDurationFirstPaint
|
package/lib/support/cli.js
CHANGED
|
@@ -584,6 +584,12 @@ module.exports.parseCommandLine = function parseCommandLine() {
|
|
|
584
584
|
'Keep the original video. Use it when you have a Visual Metrics bug and want to create an issue at GitHub',
|
|
585
585
|
group: 'video'
|
|
586
586
|
})
|
|
587
|
+
.option('videoParams.thumbsize', {
|
|
588
|
+
default: 400,
|
|
589
|
+
describe:
|
|
590
|
+
'The maximum size of the thumbnail in the filmstrip. Default is 400 pixels in either direction. If videoParams.filmstripFullSize is used that setting overrides this.',
|
|
591
|
+
group: 'video'
|
|
592
|
+
})
|
|
587
593
|
.option('videoParams.filmstripFullSize', {
|
|
588
594
|
type: 'boolean',
|
|
589
595
|
default: false,
|
|
@@ -52,7 +52,13 @@ module.exports = function (videoMetrics, timingMetrics, options) {
|
|
|
52
52
|
value: vm.LastVisualChange
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
|
-
|
|
55
|
+
|
|
56
|
+
if (vm.LargestContentfulPaint) {
|
|
57
|
+
metricsAndValues.push({
|
|
58
|
+
name: 'LargestContentfulPaint',
|
|
59
|
+
value: vm.LargestContentfulPaint
|
|
60
|
+
});
|
|
61
|
+
} else if (timingMetrics) {
|
|
56
62
|
const pt = timingMetrics;
|
|
57
63
|
metricsAndValues.push({
|
|
58
64
|
name: 'DOMContentLoaded',
|
|
@@ -58,6 +58,8 @@ module.exports = {
|
|
|
58
58
|
false
|
|
59
59
|
);
|
|
60
60
|
|
|
61
|
+
const thumbsize = get(options, 'videoParams.thumbsize', 400);
|
|
62
|
+
|
|
61
63
|
const scriptArgs = [
|
|
62
64
|
'--video',
|
|
63
65
|
videoPath,
|
|
@@ -112,6 +114,9 @@ module.exports = {
|
|
|
112
114
|
scriptArgs.unshift('--dir', imageDirPath);
|
|
113
115
|
if (fullSizeFilmstrip) {
|
|
114
116
|
scriptArgs.push('--full');
|
|
117
|
+
} else if (thumbsize !== 400) {
|
|
118
|
+
scriptArgs.push('--thumbsize');
|
|
119
|
+
scriptArgs.push(thumbsize);
|
|
115
120
|
}
|
|
116
121
|
}
|
|
117
122
|
|