-
Notifications
You must be signed in to change notification settings - Fork 9.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
core(lantern): improve LCP tail error #10529
Changes from all commits
a57f352
e76c0a8
d79e310
4bca67b
2551c56
ac5f1f5
9f58163
65fe3c8
48e2db7
27885d4
269a9b5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,6 +29,15 @@ const NodeState = { | |
Complete: 3, | ||
}; | ||
|
||
/** @type {Record<NetworkNode['record']['priority'], number>} */ | ||
const PriorityStartTimePenalty = { | ||
VeryHigh: 0, | ||
High: 0.25, | ||
Medium: 0.5, | ||
Low: 1, | ||
VeryLow: 2, | ||
}; | ||
|
||
/** @type {Map<string, LH.Gatherer.Simulation.Result['nodeTimings']>} */ | ||
const ALL_SIMULATION_NODE_TIMINGS = new Map(); | ||
|
||
|
@@ -60,7 +69,7 @@ class Simulator { | |
this._cpuSlowdownMultiplier = this._options.cpuSlowdownMultiplier; | ||
this._layoutTaskMultiplier = this._cpuSlowdownMultiplier * this._options.layoutTaskMultiplier; | ||
/** @type {Array<Node>} */ | ||
this._cachedNodeListByStartTime = []; | ||
this._cachedNodeListByStartPosition = []; | ||
|
||
// Properties reset on every `.simulate` call but duplicated here for type checking | ||
this._flexibleOrdering = false; | ||
|
@@ -103,7 +112,7 @@ class Simulator { | |
this._numberInProgressByType = new Map(); | ||
|
||
this._nodes = {}; | ||
this._cachedNodeListByStartTime = []; | ||
this._cachedNodeListByStartPosition = []; | ||
// NOTE: We don't actually need *all* of these sets, but the clarity that each node progresses | ||
// through the system is quite nice. | ||
for (const state of Object.values(NodeState)) { | ||
|
@@ -144,11 +153,12 @@ class Simulator { | |
* @param {number} queuedTime | ||
*/ | ||
_markNodeAsReadyToStart(node, queuedTime) { | ||
const firstNodeIndexWithGreaterStartTime = this._cachedNodeListByStartTime | ||
.findIndex(candidate => candidate.startTime > node.startTime); | ||
const insertionIndex = firstNodeIndexWithGreaterStartTime === -1 ? | ||
this._cachedNodeListByStartTime.length : firstNodeIndexWithGreaterStartTime; | ||
this._cachedNodeListByStartTime.splice(insertionIndex, 0, node); | ||
const nodeStartPosition = Simulator._computeNodeStartPosition(node); | ||
const firstNodeIndexWithGreaterStartPosition = this._cachedNodeListByStartPosition | ||
.findIndex(candidate => Simulator._computeNodeStartPosition(candidate) > nodeStartPosition); | ||
const insertionIndex = firstNodeIndexWithGreaterStartPosition === -1 ? | ||
this._cachedNodeListByStartPosition.length : firstNodeIndexWithGreaterStartPosition; | ||
this._cachedNodeListByStartPosition.splice(insertionIndex, 0, node); | ||
|
||
this._nodes[NodeState.ReadyToStart].add(node); | ||
this._nodes[NodeState.NotReadyToStart].delete(node); | ||
|
@@ -160,8 +170,8 @@ class Simulator { | |
* @param {number} startTime | ||
*/ | ||
_markNodeAsInProgress(node, startTime) { | ||
const indexOfNodeToStart = this._cachedNodeListByStartTime.indexOf(node); | ||
this._cachedNodeListByStartTime.splice(indexOfNodeToStart, 1); | ||
const indexOfNodeToStart = this._cachedNodeListByStartPosition.indexOf(node); | ||
this._cachedNodeListByStartPosition.splice(indexOfNodeToStart, 1); | ||
|
||
this._nodes[NodeState.InProgress].add(node); | ||
this._nodes[NodeState.ReadyToStart].delete(node); | ||
|
@@ -203,9 +213,9 @@ class Simulator { | |
/** | ||
* @return {Node[]} | ||
*/ | ||
_getNodesSortedByStartTime() { | ||
_getNodesSortedByStartPosition() { | ||
// Make a copy so we don't skip nodes due to concurrent modification | ||
return Array.from(this._cachedNodeListByStartTime); | ||
return Array.from(this._cachedNodeListByStartPosition); | ||
} | ||
|
||
/** | ||
|
@@ -456,7 +466,7 @@ class Simulator { | |
// loop as long as we have nodes in the queue or currently in progress | ||
while (nodesReadyToStart.size || nodesInProgress.size) { | ||
// move all possible queued nodes to in progress | ||
for (const node of this._getNodesSortedByStartTime()) { | ||
for (const node of this._getNodesSortedByStartPosition()) { | ||
this._startNodeIfPossible(node, totalElapsedTime); | ||
} | ||
|
||
|
@@ -500,6 +510,17 @@ class Simulator { | |
static get ALL_NODE_TIMINGS() { | ||
return ALL_SIMULATION_NODE_TIMINGS; | ||
} | ||
|
||
/** | ||
* We attempt to start nodes by their observed start time using the record priority as a tie breaker. | ||
* When simulating, just because a low priority image started 5ms before a high priority image doesn't mean | ||
* it would have happened like that when the network was slower. | ||
* @param {Node} node | ||
*/ | ||
static _computeNodeStartPosition(node) { | ||
if (node.type === 'cpu') return node.startTime; | ||
return node.startTime + (PriorityStartTimePenalty[node.record.priority] * 1000 * 1000 || 0); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it shouldn't (it ran successfully on our entire lantern corpus and passed all our tests without this change) but in the event that we get something unplanned for priority the failure mode without it is no longer sorting the nodes at all, failing the entire performance category also seems like a pretty bad experience so 0 fallback makes the most sense. |
||
} | ||
} | ||
|
||
module.exports = Simulator; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wait, this seems different than just a tie breaker. It can actually change the order of nodes that don't have the same start time?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"tiebreaker" if it were only the exact same nanosecond timestamp is useless. The lower the priority the wider the berth that's given to consider it a "tie" and defer to the higher priority request.
Requests started within the same second that all have their dependencies finished do not really need to be started in the exact same observed order that Chrome happened to request them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure, but usually "tie breaker" is for exactly that sort of thing (e.g. sorting comparators). This is more of a time spreading heuristic :)
You didn't post much motivation for the exact numbers used in the PR, and it's not clear to me that reordering like that would work out correctly, but I guess the proof is the comparison errors :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bingo :) haha
and that's fair, so here's my caveat to anyone who comes back trying to find where they came from
Dear reader from the future, I do not have supreme confidence that the exact numbers used in the PR are optimal nor were they divined after some extensive research process. They "felt right" and improved accuracy so if you need to change them for similar reasons, feel free :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lmao i am future reader, thx for context